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 81c0c457c8..1e074c027d 100644 --- a/.github/skills/sdk-java-e2e-test/SKILL.md +++ b/.github/skills/sdk-java-e2e-test/SKILL.md @@ -1,6 +1,6 @@ --- name: sdk-java-e2e-test -description: "Use this skill when creating a Java SDK E2E integration test backed by a replay proxy YAML snapshot" +description: "Use this skill when creating a Java SDK surface-area E2E integration test backed by a replay proxy YAML snapshot" --- # Creating a New Java E2E Test with a Replay Proxy YAML Snapshot @@ -9,13 +9,12 @@ description: "Use this skill when creating a Java SDK E2E integration test backe 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. +that directory exists; otherwise use the repository root itself. All paths below +are relative to that SDK root. -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 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 @@ -23,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 @@ -142,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 @@ -153,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: @@ -171,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 | -**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: +## Tests that call built-in tools + +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 e9b2e86fa0..0000000000 --- a/.github/skills/sdk-java-e2e-test/examples.md +++ /dev/null @@ -1,177 +0,0 @@ -# Examples: New Java E2E Test with YAML Snapshot - -## 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/block-remove-before-merge.yml b/.github/workflows/block-remove-before-merge.yml index bc57bbb5d7..443eea9f7c 100644 --- a/.github/workflows/block-remove-before-merge.yml +++ b/.github/workflows/block-remove-before-merge.yml @@ -8,7 +8,7 @@ on: merge_group: permissions: - pull-requests: read + contents: read jobs: check-paths: @@ -16,19 +16,25 @@ jobs: if: github.event_name == 'pull_request' && github.base_ref == 'main' runs-on: ubuntu-latest steps: + - uses: actions/checkout@v7 + timeout-minutes: 4 + with: + fetch-depth: 2 - name: Check for remove-before-merge paths in PR - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} - REPO: ${{ github.repository }} run: | - FILES=$(gh api repos/$REPO/pulls/$PR_NUMBER/files --paginate --jq '.[].filename') - BLOCKED=$(echo "$FILES" | grep -E '(^|/)[-a-zA-Z0-9_]+-remove-before-merge(/|$)' || true) - if [ -n "$BLOCKED" ]; then + # pull_request checks out the merge commit; its first parent is the base. + git rev-parse --verify HEAD^2 >/dev/null 2>&1 || { + echo "::error::Expected a pull request merge commit." + exit 1 + } + FILES_FILE=$(mktemp) + trap 'rm -f "$FILES_FILE"' EXIT + git diff --name-only -z HEAD^1 HEAD > "$FILES_FILE" + if grep -zqE '(^|/)[-a-zA-Z0-9_]+-remove-before-merge(/|$)' "$FILES_FILE"; then echo "::error::This PR contains files under a 'remove-before-merge' directory. Remove them before merging." echo "" echo "Offending paths:" - echo "$BLOCKED" + grep -zE '(^|/)[-a-zA-Z0-9_]+-remove-before-merge(/|$)' "$FILES_FILE" | tr '\0' '\n' exit 1 fi echo "No remove-before-merge paths found. ✅" 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/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 f737a8c811..48531f4c2a 100644 --- a/.github/workflows/sdk-dotnet.yml +++ b/.github/workflows/sdk-dotnet.yml @@ -1,5 +1,5 @@ # Invoked by sdk.yml to run the .NET SDK build/test matrix, documentation, -# backend shards, full subprocess CAPI coverage, and focused in-process smoke +# full subprocess CAPI coverage, and focused in-process smoke # coverage on Linux and x64 musl. name: "SDK .NET" @@ -25,13 +25,9 @@ permissions: jobs: dotnet-linux-x64: - name: ".NET (ubuntu-latest, ${{ matrix.transport }}, CAPI, full)" + 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,17 +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' || '' }} - DOTNET_TEST_SHARD: full - 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: . @@ -62,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: >- @@ -77,61 +70,53 @@ 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-full-${{ 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, ${{ matrix.shard }})" + name: ".NET (macos-latest, default, CAPI)" if: inputs.platform == 'all' || inputs.platform == 'darwin-arm64' runs-on: macos-latest timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - shard: - [ - "1", - "2a", - "2b-pending", - "2b-permission", - "2b-auth", - "2b-hooks", - "2b-unit-p", - "2b-provider", - "2b-rpc-additional", - "2b-rpc-agent", - "2b-rpc-event-log", - "2b-rpc-event-effects", - "2b-rpc-mcp-skills", - "2b-rpc-mcp-config", - "2b-rpc-mcp-lifecycle", - "2b-rpc-q-z", - "extensions", - "2c", - ] defaults: run: shell: bash working-directory: ${{ inputs.sdk-home }}/dotnet env: COPILOT_SDK_E2E_BACKEND: capi - DOTNET_TEST_SHARD: ${{ matrix.shard }} 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: . @@ -151,36 +136,31 @@ jobs: - if: failure() uses: actions/upload-artifact@v7 with: - name: dotnet-test-diagnostics-macos-latest-default-capi-${{ matrix.shard }}-${{ github.run_attempt }} + name: dotnet-test-diagnostics-macos-latest-default-capi-${{ github.run_attempt }} path: ${{ inputs.sdk-home }}/dotnet/TestResults/ if-no-files-found: warn retention-days: 7 dotnet-win32-x64: - name: ".NET (windows-latest, default, CAPI, ${{ matrix.shard }})" + name: ".NET (windows-latest, default, CAPI)" if: inputs.platform == 'all' || inputs.platform == 'win32-x64' runs-on: windows-latest timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - shard: ["1", "2"] defaults: run: shell: bash working-directory: ${{ inputs.sdk-home }}/dotnet env: COPILOT_SDK_E2E_BACKEND: capi - DOTNET_TEST_SHARD: ${{ matrix.shard }} DOTNET_TEST_RUNTIME: win-x64 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: . @@ -201,81 +181,26 @@ jobs: - if: failure() uses: actions/upload-artifact@v7 with: - name: dotnet-test-diagnostics-windows-latest-default-capi-${{ matrix.shard }}-${{ 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" - DOTNET_TEST_SHARD: full - 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: - 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-ubuntu-latest-default-${{ matrix.backend }}-full-${{ 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: . @@ -287,7 +212,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 @@ -310,50 +234,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..6ca019a1f5 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] 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 29927820e2..752670df1a 100644 --- a/.github/workflows/sdk-java.yml +++ b/.github/workflows/sdk-java.yml @@ -40,9 +40,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: 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: . @@ -63,9 +63,8 @@ jobs: run: ./mvnw javadoc:javadoc - if: github.event_name != 'merge_group' && matrix.test-jdk == '25' run: ./mvnw spotless:check - - run: npm ci --ignore-scripts - working-directory: ${{ inputs.sdk-home }}/test/harness - 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" @@ -78,6 +77,32 @@ 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' && 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' && 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: @@ -86,15 +111,16 @@ jobs: ${{ 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) + if: contains(fromJSON('["all","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"}]') }} + include: ${{ fromJSON(inputs.platform == 'darwin-arm64' && '[{"os":"macos-26","classifier":"darwin-arm64"}]' || inputs.platform == 'win32-x64' && '[{"os":"windows-latest","classifier":"win32-x64"}]' || '[{"os":"windows-latest","classifier":"win32-x64"},{"os":"macos-26","classifier":"darwin-arm64"}]') }} runs-on: ${{ matrix.os }} defaults: run: @@ -105,9 +131,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: . @@ -121,14 +147,12 @@ jobs: 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 - if: failure() uses: actions/upload-artifact@v7 with: @@ -149,9 +173,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-musl + artifact-name: executable-sdk-Linux-X64-musl path: ${{ github.workspace }} - run: node "$SDK_HOME/scripts/ci/runtime-artifact.mjs" restore working-directory: . @@ -195,27 +219,3 @@ jobs: ${{ inputs.sdk-home }}/java/sdk/target/surefire-reports-isolated/ ${{ inputs.sdk-home }}/java/sdk/target/failsafe-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..1c311b3924 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] 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..66ef2fadca 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] 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 04ced65849..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,19 @@ 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 id: cache if: inputs.runtime-source == 'checkout' @@ -78,21 +177,32 @@ jobs: 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 src/native/runtime/runtime.*.node.metadata - src/native/runtime/copilot-runtime.* + src/native/runtime/copilot-runtime.*.profile src/native/runtime/index.d.ts - src/native/cli/cli-native.*.node src/native/cli/index.d.ts - key: ${{ inputs.shared-cli-build-compatible && format('project-build-v1-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-v1-{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) }} + !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: inputs.runtime-source == 'checkout' && steps.cache.outputs.cache-hit == 'true' + run: node script/restore-cli-build.mjs - if: inputs.runtime-source == 'checkout' && steps.cache.outputs.cache-hit != 'true' 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: @@ -159,16 +269,19 @@ jobs: 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 src/native/runtime/runtime.*.node.metadata - src/native/runtime/copilot-runtime.* + src/native/runtime/copilot-runtime.*.profile src/native/runtime/index.d.ts - src/native/cli/cli-native.*.node src/native/cli/index.d.ts + !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 @@ -177,6 +290,8 @@ jobs: 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 diff --git a/.github/workflows/sdk-rust.yml b/.github/workflows/sdk-rust.yml index ff875cda73..5ab3134139 100644 --- a/.github/workflows/sdk-rust.yml +++ b/.github/workflows/sdk-rust.yml @@ -25,7 +25,7 @@ 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: @@ -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() && runner.os != 'Windows' && (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..979c24425e 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 @@ -49,8 +52,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 +74,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 +95,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 +131,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 @@ -307,15 +317,22 @@ jobs: sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} secrets: inherit - sdk-docs: - name: "Documentation" - needs: detect-layout - if: github.event_name != 'merge_group' - uses: ./.github/workflows/sdk-platform.yml - with: - platform: docs - sdk-home: ${{ needs.detect-layout.outputs.sdk-home }} - secrets: inherit + # Only Linux CAPI gates this rollup; the full SDK aggregate still reports all coverage. + sdk-typescript: + name: sdk-typescript + if: always() + needs: [sdk-linux-x64] + runs-on: ubuntu-slim + steps: + - name: Check Linux CAPI Node.js SDK result + env: + RESULTS: ${{ toJSON(needs) }} + run: | + 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" @@ -408,7 +425,6 @@ jobs: - sdk-linuxmusl-x64 - sdk-darwin-arm64 - sdk-win32-x64 - - sdk-docs - check-freshness runs-on: ubuntu-slim steps: @@ -435,7 +451,6 @@ jobs: sdk-linuxmusl-x64 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 +466,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 diff --git a/AGENTS.md b/AGENTS.md index a32dc23f11..9a9808539d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,6 +49,13 @@ ## Testing & E2E tips ⚙️ +- In the runtime repository, write new E2E tests for shared runtime behavior + available entirely through the SDK in the TypeScript SDK suite + (`/nodejs/test/e2e/`), not `test/cli/e2e/`. Use CLI E2Es for + terminal UI and CLI-only behavior. Add E2Es in Python, Go, .NET, Rust, or + Java only for that language's SDK surface area, not for shared runtime + functionality. In the runtime repository, invoke the `e2e-test-author` and + `writing-tests` skills before writing TypeScript SDK E2Es. - E2E runs against a local **replaying CAPI proxy** (see `/test/harness/server.ts`). Most language E2E harnesses spawn that server automatically (see `/python/e2e/testharness/proxy.py`). - Tests rely on YAML snapshot exchanges under `/test/snapshots/` — to add test scenarios, add or edit the appropriate YAML files and update tests. - The harness prints `Listening: http://...` — tests parse this URL to configure CLI or proxy. @@ -82,7 +89,7 @@ - SDK code: `/nodejs/src`, `/python/copilot`, `/go`, `/dotnet/src`, `/rust/src`, `/java/sdk/src/main/java` - Unit tests: `/nodejs/test`, `/python/*`, `/go/*`, `/dotnet/test`, `/rust/tests`, `/java/sdk/src/test/java` -- E2E tests: `*/e2e/` folders that use the shared replay proxy and `/test/snapshots/`, `/java/sdk/src/test/java/**/e2e/` +- E2E tests: `/nodejs/test/e2e/` for SDK-accessible runtime behavior; other `*/e2e/` folders and `/java/sdk/src/test/java/**/e2e/` for language-specific SDK behavior, using the shared replay proxy and `/test/snapshots/` - Generated types: in the runtime repository, run `npm --prefix run generate` or `generate:` to derive committed schemas and clients from runtime HEAD. In the standalone SDK repository, the same commands use the pinned Copilot CLI release schemas. Update the pin only when intentionally advancing standalone generation inputs. - For schema-only generation, conditional freshness checks, and protocol generation, follow the contributor guide's 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 509237ca8d..ebf27c92a8 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`; @@ -172,6 +197,17 @@ Language-specific details: - [Rust](rust/README.md#development) - [Java](java/README.md#development-setup) +### E2E test ownership + +Keep end-to-end coverage of the runtime's individual generated RPC methods, +their response shapes, and built-in tool behavior in the TypeScript SDK +(`nodejs/test/e2e/`). The .NET, Go, Python, Rust, and Java suites retain a +small number of generated RPC round trips to verify each language's transport +and generated bindings, but should not repeat the per-method or built-in tool +matrices. Their remaining E2E tests should exercise handwritten SDK behavior +such as connection lifecycle, session orchestration, callbacks, custom tools, +configuration, and language-specific integrations. + ### Testing an unreleased runtime API In `github/copilot-agent-runtime`, Rust contracts under @@ -265,10 +301,13 @@ export COPILOT_CLI_PATH="$(npm --prefix nodejs run --silent prepare:runtime -- - export COPILOT_LEGACY_CLI_PATH="$(npm --prefix nodejs run --silent prepare:runtime -- --print-legacy-path)" npm --prefix nodejs test -- test/e2e/structured_output.e2e.test.ts (cd dotnet && dotnet test test/GitHub.Copilot.SDK.Test.csproj \ + -p:CopilotSkipCliDownload=true \ --filter FullyQualifiedName~StructuredOutputE2ETests) ``` These are shell-local overrides for focused runs, not machine-wide settings. +The .NET flag skips MSBuild's separate release download; the tests use the +prepared runtime from `COPILOT_CLI_PATH`. The facade sets runtime paths only for its own child processes and clears stale or cross-target overrides before building the host CLI. Cross-target CI instead stages explicit artifacts and uses native test commands; do not copy @@ -277,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 @@ -294,6 +338,59 @@ follow [the Rust SDK workflow](.github/workflows/sdk-rust.yml) for rustdoc. ### Recording and replaying SDK tests +For new E2E coverage of runtime functionality available entirely through the +SDK, use the TypeScript SDK suite in `nodejs/test/e2e/` rather than the CLI +suite in the runtime repository. CLI E2Es are for terminal interactions, +rendered UI, CLI-only commands or flags, and other CLI-specific contracts. +Add E2Es in other SDK languages only when they test that language's SDK surface +area, not shared runtime behavior. + +For TypeScript SDK E2Es, use the existing +`nodejs/test/e2e/harness/sdkTestContext.ts` fixture. In the runtime repository, +also follow the `e2e-test-author` skill's SDK section. + +SDK CI runs full subprocess coverage followed by a short in-process smoke +step on the same runner wherever both modes share a platform. Smoke still +runs if the subprocess tests fail, and either failure fails the job. Java's +macOS, Windows, and musl smoke jobs remain separate because they have no +matching subprocess job. Merge groups retain the reduced Linux TypeScript +CAPI subprocess coverage. + +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 @@ -318,10 +415,12 @@ environment. If the host proxy substitutes a protected credential, set `GITHUB_TOKEN="$GH_TOKEN"` using its issued placeholder; do not print or persist the credential. Keep localhost and loopback in `NO_PROXY`. -Equivalent cross-language E2Es must share snapshot names and prompts, not -language-specific copies. The structured-output suite in **all six SDKs** reuses -the following captures in `test/snapshots/structured_output/`, recorded using -real CAPI `gpt-4.1` calls through the shared harness: +Where E2Es for equivalent language-specific SDK APIs are needed in multiple +languages, share snapshot names and prompts instead of making language-specific +copies. The existing structured-output suite in **all six SDKs** reuses the +following captures in `test/snapshots/structured_output/`, recorded using real +CAPI `gpt-4.1` calls through the shared harness. New shared-runtime E2Es do not +need copies across languages: | Shared capture (without `.yaml`) | Flow | | --- | --- | @@ -336,16 +435,18 @@ real CAPI `gpt-4.1` calls through the shared harness: 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. - -Every language additionally checks rejection before admission and zero provider -calls for oversized schemas and typed immediate steering. These cases have no -model responses and therefore need **no snapshot**. Do not create canned responses -or empty model captures for them. Unit tests supplement, rather than replace, -the shared runtime E2Es. +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. +These cases have no model responses and therefore need **no snapshot**. Do not +create canned responses or empty model captures for them. Unit tests supplement, +rather than replace, the shared runtime E2Es. ## Submitting a Pull Request diff --git a/docs/developer-docs/secrets.md b/docs/developer-docs/secrets.md index ff7bd7d79c..1218f96254 100644 --- a/docs/developer-docs/secrets.md +++ b/docs/developer-docs/secrets.md @@ -1,16 +1,16 @@ # 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 @@ -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/streaming-events.md b/docs/features/streaming-events.md index 0292f98e00..514a8b2b0e 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` @@ -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/runtime-supervised-host.md b/docs/runtime-supervised-host.md index f37bcb7af9..427fe39daf 100644 --- a/docs/runtime-supervised-host.md +++ b/docs/runtime-supervised-host.md @@ -24,12 +24,13 @@ target architecture; application callbacks use ordinary SDK RPC. ## Ownership and transport -The application connects to a runtime in the usual way. `client.startAhpHost()` +The application connects to a runtime in the usual way. `client.startAhpHost(options)` asks that runtime to start the host library in a task. An in-memory duplex stream carries ordinary SDK JSON-RPC on a separate connection to that same runtime. This is not an AHP transport stream. -The hosting task owns the physical, authenticated AHP listener. The requesting SDK +The hosting task owns the selected local WebSocket listener and/or GitHub Mission +Control registration and WPS connections. The requesting SDK connection owns the task. Disposing the handle, losing that connection, or shutting down the runtime ends the host's participation. Cleanup must not independently delete sessions or terminate another owner's work. A new SDK @@ -56,6 +57,7 @@ detached, and the hosting task has stopped, not merely that shutdown was request ```ts await client.start(); const host = await client.startAhpHost({ + localServer: {}, onExit: (exit) => console.log(`AHP host stopped: ${exit.reason}`), }); // Connect an AHP client using host.url and, when defined, host.token. @@ -63,7 +65,9 @@ const host = await client.startAhpHost({ await client.stop(); ``` -`AhpHostOptions` also accepts `hostname`, `port`, `token`, and +`AhpHostOptions` requires at least one explicit transport: `localServer`, +`githubEnvironment`, or both. There is no implicit local listener. +`localServer` accepts `hostname`, `port`, `token`, and `requireConnectionToken`. The hostname defaults to `127.0.0.1`; explicit non-loopback addresses are allowed. An omitted or zero port selects an available port; other values must be integers from 1 through 65535. The returned URL contains @@ -82,6 +86,57 @@ Node.js, Rust, Python, Go, .NET, and Java expose experimental thin handles over `onExit` is a local callback, not part of the serialized start request. There is no `closed` promise and no public generic notification-registration API. +### GitHub Mission Control hosting + +```ts +const host = await client.startAhpHost({ + githubEnvironment: { + name: "My application", + computeId: "stable-application-installation-id", + }, + // Include localServer: {} to also enable a local listener. +}); +console.log(host.environmentId); +``` + +Both `name` and `computeId` are required. Keep the application-supplied compute ID +stable across restarts to re-adopt the environment. The runtime uses its existing +authenticated GitHub identity; a local listener token is not an MC credential. +Startup fails if a requested transport cannot become ready, rather than silently +downgrading to local-only hosting. Select transports at startup; dispose and +recreate the host to change them. + +GitHub-only hosting does not open a local listener. Its host handle has an +environment ID but no local URL or connection token. `environmentId` is absent +for local-only hosting. Disposal ends transport and registration activity without +deleting the saved MC environment record or application-owned sessions. + +Registering an environment does not publish every application session. Use +`publishSession` for existing resident sessions; factory callbacks and durable +catalog behavior are unchanged. Environment management is independent of a +running host and available only through generated `rpc.environments.list`, +`rpc.environments.get`, and `rpc.environments.delete` operations, using each +language's naming conventions. + +```ts +const { environments } = await client.rpc.environments.list({ + kind: "user-local", + status: "online", +}); +const selected = environments[0]; +if (selected) { + const { environment } = await client.rpc.environments.get({ + environmentId: selected.id, + }); + // Delete only when the application intends to remove this saved environment. + await client.rpc.environments.delete({ environmentId: environment.id }); +} +``` + +These management operations are experimental and do not require a running host. +Discovery returns safe metadata, not host-side relay credentials. GitHub-managed +environments cannot be deleted through this API. + ## Application-owned sessions Application session factories optionally handle fresh AHP @@ -100,6 +155,7 @@ import { approveAll, type CopilotClient } from "@github/copilot-sdk"; async function startApplicationHost(client: CopilotClient) { return client.startAhpHost({ + localServer: {}, createSession: ({ config, signal }) => { signal.throwIfAborted(); return client.createSession({ @@ -121,11 +177,13 @@ async function startApplicationHost(client: CopilotClient) { ```rust use std::sync::Arc; use github_copilot_sdk::{AhpHost, AhpHostOptions, AhpSessionRequest, Client, Error}; +use github_copilot_sdk::rpc::HostLocalServerOptions; async fn start_application_host(owner: &Client) -> Result { owner .start_ahp_host( AhpHostOptions::new() + .with_local_server(HostLocalServerOptions::default()) .with_create_session( |request: AhpSessionRequest, client: Client| async move { Ok(Arc::new(client.create_session(request.config).await?)) @@ -153,6 +211,7 @@ from copilot import ( AhpHostOptions, AhpSessionCreateRequest, CopilotClient, CopilotSession, PermissionHandler, ) +from copilot.rpc import HostLocalServerOptions async def start_application_host(client: CopilotClient): async def create(request: AhpSessionCreateRequest): @@ -164,7 +223,11 @@ async def start_application_host(client: CopilotClient): await session.disconnect() return await client.start_ahp_host( - AhpHostOptions(create_session=create, on_session_released=release) + AhpHostOptions( + local_server=HostLocalServerOptions(), + create_session=create, + on_session_released=release, + ) ) ``` @@ -178,10 +241,12 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func startApplicationHost(ctx context.Context, client *copilot.Client) (*copilot.AhpHost, error) { return client.StartAhpHost(ctx, &copilot.AhpHostOptions{ + LocalServer: &rpc.HostLocalServerOptions{}, CreateSession: func(ctx context.Context, request copilot.AhpSessionCreateRequest) (*copilot.Session, error) { request.Config.OnPermissionRequest = copilot.PermissionHandler.ApproveAll return client.CreateSession(ctx, request.Config) @@ -209,6 +274,7 @@ static class HostingExample public static Task StartApplicationHostAsync(CopilotClient client) => client.StartAhpHostAsync(new AhpHostOptions { + LocalServer = new(), CreateSession = request => { request.CancellationToken.ThrowIfCancellationRequested(); @@ -229,6 +295,7 @@ import com.github.copilot.AhpHost; import com.github.copilot.AhpHostOptions; import com.github.copilot.AllowCopilotExperimental; import com.github.copilot.CopilotClient; +import com.github.copilot.generated.rpc.HostLocalServerOptions; import com.github.copilot.rpc.PermissionHandler; import java.util.concurrent.CompletableFuture; @@ -236,6 +303,7 @@ import java.util.concurrent.CompletableFuture; class HostingExample { static CompletableFuture startApplicationHost(CopilotClient client) { return client.startAhpHost(new AhpHostOptions() + .setLocalServer(new HostLocalServerOptions(null, null, null, null)) .setCreateSession(request -> client.createSession(request.config() .setOnPermissionRequest(PermissionHandler.APPROVE_ALL))) .setOnSessionReleased(session -> { 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 b794e454cd..6bde71b71c 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -37,7 +37,14 @@ dotnet run --file dotnet/samples/ManualToolResume.cs ## Quick Start -For experimental in-process AHP hosting, use `client.StartAhpHostAsync(new AhpHostOptions { ... })`. +For experimental in-process AHP hosting, select a transport explicitly: +`client.StartAhpHostAsync(new AhpHostOptions { LocalServer = new() })`. +For GitHub Mission Control, set +`GitHubEnvironment = new() { Name = "My host", ComputeId = "compute-id" }` +instead, or configure both transports. GitHub environment name and compute ID are +required; there is no implicit local listener. The host's `Url`, `Token`, and `Pid` +are nullable; `EnvironmentId` contains the GitHub environment ID when configured. +Environment list/get/delete operations are available only through the generated RPC API. See [runtime-supervised AHP hosting](../docs/runtime-supervised-host.md) for creation and resume callbacks, resident-session publication, ownership, and shared-snapshot E2Es. @@ -103,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 @@ -218,6 +226,16 @@ 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`. - `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 @@ -340,6 +358,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 @@ -488,6 +519,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()`. @@ -780,6 +818,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: @@ -958,6 +1006,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 @@ -1246,6 +1296,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 4905002cc0..999440992f 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -291,7 +291,7 @@ private static RuntimeConnection ResolveDefaultConnection(CopilotClientOptions o var value = options.Environment is not null && options.Environment.TryGetValue(DefaultConnectionEnvVar, out var fromOptions) ? fromOptions - : Environment.GetEnvironmentVariable(DefaultConnectionEnvVar); + : System.Environment.GetEnvironmentVariable(DefaultConnectionEnvVar); if (string.IsNullOrEmpty(value) || string.Equals(value, "stdio", StringComparison.OrdinalIgnoreCase)) { @@ -699,7 +699,7 @@ or IOException if (ctx.FfiHost is { } ffiHost) { - try { ffiHost.Dispose(); } + try { await Task.Run(ffiHost.Dispose).ConfigureAwait(false); } catch (Exception ex) { AddCleanupError(errors, ex, _logger); } _ffiHost = null; } @@ -1208,7 +1208,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 +1229,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) @@ -1343,6 +1346,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 +1354,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance transformCallbacks, hasHooks, "CopilotClient.CreateSessionAsync"); + serverAssignedSessionId = null; } }; @@ -1391,6 +1396,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 +1486,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 +1592,8 @@ 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); var rpcTimestamp = Stopwatch.GetTimestamp(); var response = await InvokeRpcAsync( @@ -1580,6 +1604,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); @@ -2194,6 +2219,11 @@ private void ConfigureSessionFsHandlers(CopilotSession session, Func? AdditionalDirectories = null); + IList? AdditionalDirectories = null, + bool? AllowTranscriptRecovery = null); #pragma warning restore GHCP001 internal record ResumeSessionResponse( @@ -3294,7 +3327,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( diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 8884365d55..6dcca141a8 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -107,6 +107,127 @@ internal sealed class ConnectRequest public string? Token { get; set; } } +/// Hosting capabilities and session capacity advertised by an environment. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class EnvironmentCapabilities +{ + /// Advertised Agent Host Protocol version. + [JsonPropertyName("ahpVersion")] + public string? AhpVersion { get; set; } + + /// Current session count, when advertised. + [JsonPropertyName("currentSessions")] + public long? CurrentSessions { get; set; } + + /// Feature identifiers advertised by the environment. + [JsonPropertyName("features")] + public IList Features { get => field ??= []; set; } + + /// Maximum session capacity, when advertised. + [JsonPropertyName("maxSessions")] + public long? MaxSessions { get; set; } +} + +/// Safe discovery information. Host-side relay bootstrap credentials are never included. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class GitHubEnvironment +{ + /// Hosting capabilities advertised by the environment. + [JsonPropertyName("capabilities")] + public EnvironmentCapabilities? Capabilities { get; set; } + + /// Identifier assigned by Mission Control. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Compute kind reported by Mission Control. + [JsonPropertyName("kind")] + public EnvironmentKind Kind { get; set; } + + /// Discovery labels attached to the environment. + [JsonPropertyName("labels")] + public IDictionary? Labels { get; set; } + + /// Timestamp of the last heartbeat received by Mission Control. + [JsonPropertyName("lastHeartbeatAt")] + public string? LastHeartbeatAt { get; set; } + + /// Human-readable environment name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Organization identifier, when the environment belongs to an organization. + [JsonPropertyName("orgId")] + public string? OrgId { get; set; } + + /// Identifier of the environment owner. + [JsonPropertyName("ownerId")] + public string? OwnerId { get; set; } + + /// Owner category reported by Mission Control. + [JsonPropertyName("ownerType")] + public string? OwnerType { get; set; } + + /// Open-ended operational status vocabulary. + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; +} + +/// Environments visible to the authenticated caller and matching the supplied filters. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class EnvironmentsListResult +{ + /// Safe discovery records without relay bootstrap credentials. + [JsonPropertyName("environments")] + public IList Environments { get => field ??= []; set; } +} + +/// Optional discovery filters supported by GitHub Mission Control. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class EnvironmentsListRequest +{ + /// Restrict discovery to this compute kind. + [JsonPropertyName("kind")] + public EnvironmentKind? Kind { get; set; } + + /// Operational status, such as online, offline, degraded, waking, or draining. + [JsonPropertyName("status")] + public string? Status { get; set; } +} + +/// Safe discovery information for the requested environment. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class EnvironmentsGetResult +{ + /// The requested environment without relay bootstrap credentials. + [JsonPropertyName("environment")] + public GitHubEnvironment Environment { get => field ??= new(); set; } +} + +/// Identify a Mission Control environment to retrieve. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class EnvironmentsGetRequest +{ + /// Identifier assigned by Mission Control. + [JsonPropertyName("environmentId")] + public string EnvironmentId { get; set; } = string.Empty; +} + +/// Acknowledgement that the requested environment was deleted. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class EnvironmentsDeleteResult +{ +} + +/// Identify a user-managed Mission Control environment to delete. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class EnvironmentsDeleteRequest +{ + /// User-managed environment to delete. GitHub-managed environments cannot be deleted. + [JsonPropertyName("environmentId")] + public string EnvironmentId { get; set; } = string.Empty; +} + /// The existing runtime identity and its resource on the listener. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class HostPublishSessionResult @@ -178,6 +299,10 @@ internal sealed class HostReleaseSessionRequest [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class HostStartResult { + /// GitHub Mission Control environment ID, present when its relay transport is ready. + [JsonPropertyName("environmentId")] + public string? EnvironmentId { get; set; } + /// Caller-generated listener UUID. [JsonPropertyName("hostId")] public string HostId { get; set; } = string.Empty; @@ -192,17 +317,26 @@ public sealed class HostStartResult /// Actual bound WebSocket URL, including the allocated port. [JsonPropertyName("url")] - public string Url { get; set; } = string.Empty; + public string? Url { get; set; } } -/// Starts a supervised AHP listener in the runtime's configured working directory. +/// GitHub Mission Control registration options. The compute ID is application-owned and stable. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class HostStartRequest +public sealed class HostGitHubEnvironmentOptions { - /// Caller-generated UUID identifying this connection-owned listener. - [JsonPropertyName("hostId")] - public string HostId { get; set; } = string.Empty; + /// Stable application installation identity, reused across host restarts. + [JsonPropertyName("computeId")] + public string ComputeId { get; set; } = string.Empty; + + /// Human-readable environment display name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} +/// Local WebSocket transport options. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class HostLocalServerOptions +{ /// Listener hostname. Defaults to 127.0.0.1; explicit non-loopback binds are allowed. [JsonPropertyName("hostname")] public string? Hostname { get; set; } @@ -215,6 +349,27 @@ internal sealed class HostStartRequest [JsonPropertyName("requireConnectionToken")] public bool? RequireConnectionToken { get; set; } + /// Nonempty connection token. Generated randomly when required and omitted. + [JsonPropertyName("token")] + public string? Token { get; set; } +} + +/// Starts a supervised AHP host with at least one explicitly selected transport. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class HostStartRequest +{ + /// Registers a GitHub Mission Control environment and enables its relay transport. + [JsonPropertyName("githubEnvironment")] + public HostGitHubEnvironmentOptions? GitHubEnvironment { get; set; } + + /// Caller-generated UUID identifying this connection-owned listener. + [JsonPropertyName("hostId")] + public string HostId { get; set; } = string.Empty; + + /// Enables a local WebSocket listener. + [JsonPropertyName("localServer")] + public HostLocalServerOptions? LocalServer { get; set; } + /// Ask the owning application to resume its durable AHP sessions. [JsonPropertyName("resumeFactory")] public bool? ResumeFactory { get; set; } @@ -222,10 +377,6 @@ internal sealed class HostStartRequest /// Ask the owning SDK application to materialize AHP sessions. [JsonPropertyName("sessionFactory")] public bool? SessionFactory { get; set; } - - /// Nonempty connection token. Generated randomly when required and omitted. - [JsonPropertyName("token")] - public string? Token { get; set; } } /// Empty acknowledgement for a completed host lifecycle operation. @@ -243,9 +394,30 @@ internal sealed class HostDisposeRequest public string HostId { get; set; } = string.Empty; } -/// Normalized listener settings delivered only to the supervised hosting participant. +/// Private credentials delivered only to a runtime-owned Mission Control hosting participant. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class HostGetConfigurationResult +internal sealed class HostGetEnvironmentCredentialsResult +{ + /// GitHub API base URL for the authenticated service. + [JsonPropertyName("githubApiUrl")] + public string GitHubApiUrl { get; set; } = string.Empty; + + /// Hostname of the authenticated GitHub service. + [JsonPropertyName("githubHost")] + public string GitHubHost { get; set; } = string.Empty; + + /// Mission Control API origin for environment registration and management. + [JsonPropertyName("missionControlUrl")] + public string MissionControlUrl { get; set; } = string.Empty; + + /// Current bearer token for the authenticated GitHub identity. + [JsonPropertyName("token")] + public string Token { get; set; } = string.Empty; +} + +/// Normalized local WebSocket listener settings. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class HostLocalServerConfiguration { /// Hostname or IP address to bind. [JsonPropertyName("hostname")] @@ -259,17 +431,30 @@ internal sealed class HostGetConfigurationResult [JsonPropertyName("requireConnectionToken")] public bool RequireConnectionToken { get; set; } + /// Secret connection token, absent when authentication is disabled. + [JsonPropertyName("token")] + public string? Token { get; set; } +} + +/// Normalized listener settings delivered only to the supervised hosting participant. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class HostGetConfigurationResult +{ + /// Requested GitHub Mission Control registration. + [JsonPropertyName("githubEnvironment")] + public HostGitHubEnvironmentOptions? GitHubEnvironment { get; set; } + + /// Normalized local listener settings, absent for relay-only hosts. + [JsonPropertyName("localServer")] + public HostLocalServerConfiguration? LocalServer { get; set; } + /// Whether app-owned durable sessions are resumed by the owning application. [JsonPropertyName("resumeFactory")] - public bool? ResumeFactory { get; set; } + public bool ResumeFactory { get; set; } /// Whether session materialization is delegated to the owning application. [JsonPropertyName("sessionFactory")] - public bool? SessionFactory { get; set; } - - /// Secret connection token, absent when authentication is disabled. - [JsonPropertyName("token")] - public string? Token { get; set; } + public bool SessionFactory { get; set; } } /// Empty acknowledgement for a completed host lifecycle operation. @@ -284,7 +469,11 @@ internal sealed class HostReadyRequest { /// Actual bound WebSocket URL. [JsonPropertyName("address")] - public string Address { get; set; } = string.Empty; + public string? Address { get; set; } + + /// Registered environment ID, reported only once the relay transport is connected. + [JsonPropertyName("environmentId")] + public string? EnvironmentId { get; set; } /// Configured secret token, absent when authentication is disabled. [JsonPropertyName("token")] @@ -387,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; } @@ -407,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; } @@ -436,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; } @@ -456,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; } @@ -548,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; } @@ -727,11 +928,11 @@ public sealed class BuiltInModelCatalog public IList Models { get => field ??= []; set; } } -/// Whether this host can run one sandbox policy feature. A session whose effective policy uses an unsupported feature fails each sandboxed command with `reason`. +/// Whether this host can run one sandbox policy feature. A session whose effective policy uses an unsupported feature fails each sandboxed command with `reason`, except `filesystem_enumeration`, whose absence degrades sandboxed PowerShell instead of failing it. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class SandboxHostCapability { - /// The policy feature, as an extensible string: ignore names you do not recognize. Known values: `network` (sandboxed commands can reach the network; on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns), `network_filtering` (host rules and the sandbox proxy; on Linux this needs the same tooling as `network`; on Windows it needs Process Security Environment 1.1 host-loopback support, and a policy that uses it must also set `network.allowLocalNetwork`), `denied_paths` (native enforcement of `filesystem.deniedPaths`), and `shell` (shell commands inside the sandbox; on Windows this needs Process Security Environment 1.1 filesystem enumeration support). + /// The policy feature, as an extensible string: ignore names you do not recognize. Known values: `network` (sandboxed commands can reach the network; on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns), `network_filtering` (host rules and the sandbox proxy; on Linux this needs the same tooling as `network`; on Windows it needs Process Security Environment 1.1 host-loopback support, and a policy that uses it must also set `network.allowLocalNetwork`), `denied_paths` (native enforcement of `filesystem.deniedPaths`), `shell` (shell commands inside the sandbox), and `filesystem_enumeration` (enumerate-only filesystem grants; on Windows this needs Process Security Environment 1.1 filesystem enumeration support, and without it sandboxed PowerShell still runs but cannot resolve its current location; other platforms always report it). [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; @@ -761,6 +962,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 @@ -880,6 +1309,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 @@ -1376,6 +1806,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)] @@ -6580,6 +7028,260 @@ public sealed class ManagedSettingsReadResult public JsonElement? SettingsJson { get; set; } } +/// One validation finding for a managed-settings document. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ManagedSettingsDiagnostic +{ + /// Human-readable description of the finding. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + + /// Dot-separated path of the offending setting, such as `autoTier.overridable`. Empty for the document as a whole. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Whether the finding rejects the document. + [JsonPropertyName("severity")] + public ManagedSettingsDiagnosticSeverity Severity { get; set; } +} + +/// One managed-settings channel and the document it delivered. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ManagedSettingsLayer +{ + /// Validated managed-settings document this channel delivered. Absent when the channel delivered none. + [JsonPropertyName("settings")] + public JsonElement? Settings { get; set; } + + /// Channel identifier: `device` (MDM, plist, registry, or managed file), `server` (account or organization policy), or `policyHelper` (session-local helper output, supported by compose). Treat unknown output values as additional channels; more may be added. + [JsonPropertyName("source")] + public string Source { get; set; } = string.Empty; +} + +/// Lock state and provenance of one managed setting. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ManagedSettingMeta +{ + /// Whether users and repositories may choose a different value. `false` means policy locks the value. + [JsonPropertyName("overridable")] + public bool Overridable { get; set; } + + /// Channel that supplied this scalar value, matching a `layers[].source`: `device`, `server`, or `policyHelper`. These scalar defaults select one winning channel, not a mixed source. Treat unknown values as additional channels; more may be added. + [JsonPropertyName("source")] + public string Source { get; set; } = string.Empty; +} + +/// Per-key lock state and provenance for `ManagedSettingsValues`, with the same field names. Producers emit each typed key in values and meta together; both outer objects are omitted when no typed key is set. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ManagedSettingsMeta +{ + /// Lock state and provenance of `values.autoTier`. + [JsonPropertyName("autoTier")] + public ManagedSettingMeta? AutoTier { get; set; } + + /// Lock state and provenance of `values.model`. + [JsonPropertyName("model")] + public ManagedSettingMeta? Model { get; set; } +} + +/// Effective enterprise managed settings and contributing channels. Session events report applied policy; sessionless resolve reports an account/device snapshot, and compose reports a non-applying preview of candidate documents. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively. Session-local SDK-client policy is included only in session results. Marked experimental while the managed-settings surface stabilizes. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ManagedSettingsResolvedData +{ + /// Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true. + [JsonPropertyName("bypassPermissionsDisabled")] + public bool BypassPermissionsDisabled { get; set; } + + /// Whether a session-local permissions layer injected by the SDK host was present. + [JsonPropertyName("clientManaged")] + public bool? ClientManaged { get; set; } + + /// Whether an actual device MDM/plist/registry/file managed-settings layer was present. + [JsonPropertyName("deviceManaged")] + public bool DeviceManaged { get; set; } + + /// Whether managed policy could not be determined (e.g. a failed server fetch) and the session fell back to the fail-closed restriction. When true, restrictions such as disabling bypass-permissions are enforced even though `settings` may be absent. + [JsonPropertyName("failClosed")] + public bool FailClosed { get; set; } + + /// The setting keys under enterprise management in the effective managed settings (e.g. `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force. + [JsonPropertyName("managedKeys")] + public IList ManagedKeys { get => field ??= []; set; } + + /// Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. + [JsonPropertyName("permissionsAllowIntersected")] + public bool? PermissionsAllowIntersected { get; set; } + + /// Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. + [JsonPropertyName("policyHelperManaged")] + public bool? PolicyHelperManaged { get; set; } + + /// Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. + [JsonPropertyName("sandboxEnabledByUndeterminedPolicy")] + public bool? SandboxEnabledByUndeterminedPolicy { get; set; } + + /// Whether the server (account/org) managed-settings layer was present. + [JsonPropertyName("serverManaged")] + public bool ServerManaged { get; set; } + + /// The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. + [JsonPropertyName("settings")] + public JsonElement? Settings { get; set; } + + /// Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. + [JsonPropertyName("source")] + public ManagedSettingsResolvedSource Source { get; set; } +} + +/// Typed effective values of managed settings. Each field mirrors the managed-settings schema key of the same name; more keys are added as they are typed. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ManagedSettingsValues +{ + /// Managed Auto routing preference, used when the selected model is `auto`. + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + + /// Managed default model identifier, as configured. New sessions start with it; it can name a model the account cannot use, so hosts match it against the listed models. + [JsonPropertyName("model")] + public string? Model { get; set; } +} + +/// Effective enterprise managed settings for an account, resolved without a session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ManagedSettingsResolveResult +{ + /// Printable opaque identity of the account the settings were resolved for, suitable for comparison and storage, not an account selectionId. Absent when no account was available, in which case only device policy is reported. + [JsonPropertyName("account")] + public string? Account { get; set; } + + /// Warnings about unavailable policy sources or a failed refresh served from cache. A cached response is not proof of a successful live fetch; `resolved.failClosed` separately describes enforcement. + [JsonPropertyName("diagnostics")] + public IList Diagnostics { get => field ??= []; set; } + + /// Each managed-settings channel consulted, strongest first, with the validated document it delivered before merging. `resolved.settings` is the merged result. More channels may be added over time. + [JsonPropertyName("layers")] + public IList Layers { get => field ??= []; set; } + + /// Per-key lock state and provenance for the entries in `values`, using the same key names. + [JsonPropertyName("meta")] + public ManagedSettingsMeta? Meta { get; set; } + + /// Effective managed settings from the device and account (server) channels, in the same shape as `session.managedSettings.get`, excluding session-local injection. + [JsonPropertyName("resolved")] + public ManagedSettingsResolvedData Resolved { get => field ??= new(); set; } + + /// Typed effective values of managed settings, keyed like the managed-settings schema and already resolved across channels, with the `{ "overridable": ... }` wrapper removed. Present when policy sets at least one typed key. Keys not typed here are available in `resolved.settings`. + [JsonPropertyName("values")] + public ManagedSettingsValues? Values { get; set; } +} + +/// RPC data type for ManagedSettingsResolve operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ManagedSettingsResolveRequest +{ + /// Embedding client identity for server policy requests, as in session creation. Omit for the CLI identity. + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } + + /// GitHub token to resolve instead of the current account. The call fails when the token cannot be resolved. + [JsonPropertyName("gitHubToken")] + public string? GitHubToken { get; set; } + + /// Opaque account identifier returned by `account.getAllUsers`. When omitted, the current account is used, or device policy only when no account is signed in. + [JsonPropertyName("selectionId")] + public string? SelectionId { get; set; } +} + +/// The authoring JSON schema for managed settings recognized by this runtime. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ManagedSettingsSchemaResult +{ + /// Version of the runtime that owns this schema. + [JsonPropertyName("runtimeVersion")] + public string RuntimeVersion { get; set; } = string.Empty; + + /// JSON schema (draft 2020-12) with descriptive shared `x-composition` annotations, not a complete runtime composition contract. Model, effortLevel, and contextTier remain coupled; use `managedSettings.compose` for the runtime's effective result. + [JsonPropertyName("schema")] + public JsonElement Schema { get; set; } +} + +/// Result of validating a managed-settings document. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ManagedSettingsValidateResult +{ + /// Errors that reject the document and warnings about content the runtime ignores. + [JsonPropertyName("diagnostics")] + public IList Diagnostics { get => field ??= []; set; } + + /// Canonical form of the document the runtime would apply, with unrecognized keys removed. Absent when the document is invalid. + [JsonPropertyName("settings")] + public JsonElement? Settings { get; set; } + + /// Whether the runtime would accept the document within the preview resource limits. Always equals whether `settings` is present. An invalid document is rejected as a whole. + [JsonPropertyName("valid")] + public bool Valid { get; set; } +} + +/// A candidate managed-settings document to validate without applying it. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ManagedSettingsValidateRequest +{ + /// The document to validate: a JSON object, or a string containing the document's JSON text. Preview documents are limited to 1 MiB and 64 levels of nesting, a stricter resource limit than delivered-policy parsing; violations are returned as diagnostics. + [JsonPropertyName("content")] + public JsonElement Content { get; set; } + + /// Channel the document is meant for (`device`, `server`, or `policyHelper`). Some keys are only honored in some channels; for example, a `policyHelper` registration is ignored in policy-helper output. When omitted, no channel-specific checks run. + [JsonPropertyName("layer")] + public string? Layer { get; set; } +} + +/// The effective managed settings the runtime would enforce for the given documents, in the same shape `managedSettings.resolve` returns. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ManagedSettingsComposeResult +{ + /// Warnings about ignored content, with paths prefixed by the channel name. + [JsonPropertyName("diagnostics")] + public IList Diagnostics { get => field ??= []; set; } + + /// Only the supplied channels, strongest first, with canonical documents. Empty canonical documents are represented as absent settings, as in live resolution. + [JsonPropertyName("layers")] + public IList Layers { get => field ??= []; set; } + + /// Per-key lock state and provenance for `values`. + [JsonPropertyName("meta")] + public ManagedSettingsMeta? Meta { get; set; } + + /// Effective managed settings, in the same shape as `session.managedSettings.get`. + [JsonPropertyName("resolved")] + public ManagedSettingsResolvedData Resolved { get => field ??= new(); set; } + + /// Typed effective values, as in `managedSettings.resolve`. + [JsonPropertyName("values")] + public ManagedSettingsValues? Values { get; set; } +} + +/// One candidate channel; absent settings represents a channel that delivered no document. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ManagedSettingsComposeLayer +{ + /// Candidate managed-settings document. Omit when the channel delivered none, as in resolve output. + [JsonPropertyName("settings")] + public JsonElement? Settings { get; set; } + + /// The channel whose candidate document is being supplied. + [JsonPropertyName("source")] + public ManagedSettingsChannel Source { get; set; } +} + +/// Candidate managed-settings documents to merge without applying them. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ManagedSettingsComposeRequest +{ + /// One entry per channel. `source` must be `device`, `server`, or `policyHelper`, each at most once (checked at runtime); order does not matter, because channel precedence is fixed. To preview documents from resolve output, map recognized source strings to ManagedSettingsChannel and copy their settings; generated resolve and compose layer types are distinct. Omitted settings means this channel delivered no document. Supplied documents must be valid within the preview limits; warnings are returned in diagnostics. Compose does not reproduce source-failure state or retained enforcement floors from resolve. + [JsonPropertyName("layers")] + public IList Layers { get => field ??= []; set; } +} + /// Indicates whether the calling client was registered as the session filesystem provider. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed class SessionFsSetProviderResult @@ -6593,6 +7295,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; } @@ -8196,83 +8902,6 @@ internal sealed class AgentRegistrySpawnRequest public AgentRegistrySpawnPermissionMode? PermissionMode { get; set; } } -/// Result of a OneAuth token acquisition. -/// Polymorphic base type discriminated by status. -[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 -{ - /// The type discriminator. - [JsonPropertyName("status")] - public virtual string Status { get; set; } = string.Empty; -} - - -/// The ok variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class EntraTokenAcquireResultOk : EntraTokenAcquireResult -{ - /// - [JsonIgnore] - public override string Status => "ok"; - - /// Opaque access token. - [JsonPropertyName("accessToken")] - public required string AccessToken { get; set; } - - /// Opaque OneAuth account id, when supplied by the broker. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("accountId")] - public string? AccountId { get; set; } - - /// Expiry as milliseconds since Unix epoch, when supplied by OneAuth. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("expiresOnTimestamp")] - public double? ExpiresOnTimestamp { get; set; } -} - -/// The interaction-required variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class EntraTokenAcquireResultInteractionRequired : EntraTokenAcquireResult -{ - /// - [JsonIgnore] - public override string Status => "interaction-required"; -} - -/// OneAuth token request supplied by a trusted host application. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class EntraTokenAcquireRequest -{ - /// Previously rejected token that OneAuth must bypass during renewal. - [JsonPropertyName("accessTokenToRenew")] - public string? AccessTokenToRenew { get; set; } - - /// Public client application id. - [JsonPropertyName("clientId")] - public string ClientId { get; set; } = string.Empty; - - /// Whether the broker may show interaction. - [JsonPropertyName("interaction")] - public EntraTokenInteraction Interaction { 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; - - /// Exact delegated scopes to request. - [JsonPropertyName("scopes")] - public IList Scopes { get => field ??= []; set; } - - /// Tenant id or tenant selector, such as common or organizations. - [JsonPropertyName("tenantId")] - public string TenantId { get; set; } = string.Empty; -} - /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] internal sealed class SessionSuspendRequest @@ -8636,4251 +9265,4070 @@ 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; } + + /// 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; } } -/// Identifies the target session. +/// An operation supported by a model-provider adapter. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSandboxGetEnforcementStatusRequest +public sealed class ModelProviderAdapterOperationDescriptor { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } + + /// Supported operation name: `discover`, `getStatus`, or `models.list`. Unknown names and duplicate declarations are rejected. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; } -/// Result of attempting to disable sandboxing for the current session. +/// Contributor attribution, independent of routing identity and authorization. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxDisableForSessionResult +public sealed class ModelProviderAttribution { - /// The authoritative sandbox enabled state after the operation. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// Human-readable contributor name, not the adapter display name. + [JsonPropertyName("ownerDisplayName")] + public string? OwnerDisplayName { get; set; } - /// Whether this call resolved the pending request and applied the session opt-out. - [JsonPropertyName("success")] - public bool Success { 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; } + + /// Kind of component that supplied the adapter. Attribution does not confer authority. + [JsonPropertyName("source")] + public ModelProviderProvenanceSource Source { get; set; } } -/// Optional informational context describing how and where the permission decision was made. This does not affect permission behavior. +/// A normalized model-provider adapter in the session's effective catalog. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionDecisionContext +public sealed class ModelProviderAdapterDescriptor { - /// Disposition of the permission request as observed by the responding client. - [JsonPropertyName("outcome")] - public PermissionDecisionOutcome Outcome { get; set; } + /// 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; - /// 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; } + /// Adapter-declared policy for passive and automatic discovery. + [JsonPropertyName("automaticDiscovery")] + public ModelProviderAutomaticDiscoveryPolicy AutomaticDiscovery { get => field ??= new(); set; } - /// Controlled reason or actor responsible for the response. - [JsonPropertyName("source")] - public PermissionDecisionSource Source { get; set; } + /// Human-readable provider name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; - /// Client surface that submitted the response. - [JsonPropertyName("surface")] - public PermissionDecisionSurface Surface { get; set; } + /// Operations supported by this provider adapter. + [JsonPropertyName("operations")] + public IList Operations { get => field ??= []; set; } + + /// Attribution for the adapter itself. + [JsonPropertyName("provenance")] + public ModelProviderAttribution Provenance { get => field ??= new(); set; } + + /// 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; } -/// Request to disable sandboxing for the current session while resolving an active sandbox-bypass permission prompt. +/// Normalized model-provider adapter definitions available to the session, not discovered instances. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SandboxDisableForSessionRequest +public sealed class ModelProviderAdapterCatalog { - /// 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; + /// 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 SessionProvidersGetCatalogRequest +{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Result of accepting a sandbox path grant. +/// Attribution for the adapter that produced a provider row. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxGrantPathForRequestResult +public sealed class ModelProviderProvenance { - /// Whether this call resolved the pending request and added the path to the session's sandbox policy. - [JsonPropertyName("success")] - public bool Success { 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; + + /// Human-readable contributor name, not the adapter display name. + [JsonPropertyName("ownerDisplayName")] + public string? OwnerDisplayName { get; set; } + + /// 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; + + /// Kind of component that supplied the adapter. + [JsonPropertyName("source")] + public ModelProviderProvenanceSource Source { get; set; } } -/// Request to accept the sandbox path grant offered on an active sandbox escalation permission prompt. +/// Serializable reference to a discovered provider instance. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SandboxGrantPathForRequestRequest +public sealed class ModelProviderInstanceReference { - /// Optional attribution for the permission decision. - [JsonPropertyName("decisionContext")] - public PermissionDecisionContext? DecisionContext { get; set; } + /// 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; - /// Identifier of the exact pending sandbox escalation permission request whose sandboxPathGrant to accept. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Stable instance identifier derived by the provider adapter, such as `ollama:{normalizedEndpoint}`. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; + + /// Descriptive provider family. Must match the selected adapter; not a routing key. + [JsonPropertyName("providerKind")] + public string ProviderKind { get; set; } = string.Empty; } -/// Authentication status and account metadata for the session. +/// A normalized model-provider instance discovered by the runtime. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionAuthStatus +public sealed class ModelProviderInstance { - /// Authentication type. - [JsonPropertyName("authType")] - public AuthInfoType? AuthType { get; set; } - - /// Copilot plan tier (e.g., individual_pro, business). - [JsonPropertyName("copilotPlan")] - public string? CopilotPlan { get; set; } + /// Human-readable instance name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; - /// Authentication host URL. + /// Inference API endpoint when the provider exposes one separately from its management endpoint. [Url] [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("host")] - public string? Host { get; set; } + [JsonPropertyName("inferenceEndpoint")] + public string? InferenceEndpoint { get; set; } - /// Whether the session has resolved authentication. - [JsonPropertyName("isAuthenticated")] - public bool IsAuthenticated { get; set; } + /// Transport to use for inference against this instance. + [JsonPropertyName("inferenceTransport")] + public ProviderEndpointTransport? InferenceTransport { get; set; } - /// Authenticated login/username, if available. - [JsonPropertyName("login")] - public string? Login { get; set; } + /// Provider family to use for inference against this instance. + [JsonPropertyName("inferenceType")] + public ProviderEndpointType? InferenceType { get; set; } - /// Human-readable authentication status description. - [JsonPropertyName("statusMessage")] - public string? StatusMessage { get; set; } -} + /// Wire API to use for inference against this instance, when required by the provider family. + [JsonPropertyName("inferenceWireApi")] + public ProviderEndpointWireApi? InferenceWireApi { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionGitHubAuthGetStatusRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } } -/// Indicates whether the credential update succeeded. +/// Typed provider-operation outcome. Use the code for control flow and the optional message for display. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSetCredentialsResult +public sealed class ModelProviderOperationOutcome { - /// 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; } + /// Machine-readable operation outcome. + [JsonPropertyName("code")] + public ModelProviderOperationOutcomeCode Code { get; set; } - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Human-readable detail for non-success outcomes. + [JsonPropertyName("message")] + public string? Message { 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. +/// Provider instances found by a discovery operation. [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 +public sealed class ModelProviderDiscoverResult { - /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; -} + /// Discovered provider instances. Empty when passive default discovery finds no reachable provider. + [JsonPropertyName("instances")] + public IList Instances { get => field ??= []; 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 input variant for GitHub-internal HMAC auth, carrying the public GitHub host and HMAC secret. -/// The hmac variant of . +/// Provider discovery parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoHmac : SettableAuthInfo +internal sealed class ModelProviderDiscoverRequest { - /// - [JsonIgnore] - public override string Type => "hmac"; - - /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("copilotUser")] - public CopilotUserResponse? CopilotUser { get; set; } + /// Opaque adapter identity returned by `session.providers.getCatalog`. + [JsonPropertyName("adapterId")] + public string AdapterId { get; set; } = string.Empty; - /// HMAC secret used to sign requests. - [JsonPropertyName("hmac")] - public required string Hmac { 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. HMAC auth always targets 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 input variant for a token sourced from an environment variable, with host, optional login, token, and env var name. -/// The env variant of . +/// Current health information for a provider instance. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoEnv : SettableAuthInfo +public sealed class ModelProviderStatus { - /// - [JsonIgnore] - public override string Type => "env"; + /// 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; } - /// Name of the environment variable the token was sourced from. - [JsonPropertyName("envVar")] - public required string EnvVar { get; set; } + /// Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`. + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; - /// Authentication host (e.g. https://github.com or a GHES host). - [JsonPropertyName("host")] - public required string Host { get; set; } + /// Provider-reported version. + [JsonPropertyName("version")] + public string? Version { 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; } +/// Provider status request parameters. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ModelProviderGetStatusRequest +{ + /// Provider instance reference returned by discovery. + [JsonPropertyName("instance")] + public ModelProviderInstanceReference Instance { get => field ??= new(); set; } - /// The token value itself. Treat as a secret. - [JsonPropertyName("token")] - public required string Token { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Token authentication accepted by session.gitHubAuth.setCredentials. -/// The token variant of . +/// Provider-reported model artifact metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoToken : SettableAuthInfo +public sealed class ModelArtifactDetails { - /// - [JsonIgnore] - public override string Type => "token"; + /// Provider-reported model architecture. + [JsonPropertyName("architecture")] + public string? Architecture { 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; } + /// Provider-reported model families. + [JsonPropertyName("families")] + public IList? Families { get; set; } - /// Authentication host. - [JsonPropertyName("host")] - public required string Host { get; set; } + /// Primary model family. + [JsonPropertyName("family")] + public string? Family { get; set; } - /// The token value itself. Treat as a secret. - [JsonPropertyName("token")] - public required string Token { 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; } } -/// Authentication-info variant for direct Copilot API token auth sourced from environment variables, with public GitHub host. -/// The copilot-api-token variant of . +/// A non-fatal provider observation warning. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoCopilotApiToken : SettableAuthInfo +public sealed class ModelProviderWarning { - /// - [JsonIgnore] - public override string Type => "copilot-api-token"; - - /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("copilotUser")] - public CopilotUserResponse? CopilotUser { get; set; } + /// Machine-readable warning code. + [JsonPropertyName("code")] + public string Code { get; set; } = string.Empty; - /// Authentication host (always the public GitHub host). - [JsonPropertyName("host")] - public required string Host { get; set; } + /// Human-readable warning message. + [JsonPropertyName("message")] + public string Message { 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 . +/// 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 partial class SettableAuthInfoUser : SettableAuthInfo +public sealed class DiscoveredModel { - /// - [JsonIgnore] - public override string Type => "user"; + /// Provider-reported model capabilities. Omitted capability fields are unknown; explicit false values are preserved. + [JsonPropertyName("capabilities")] + public ModelCapabilities Capabilities { 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; } + /// Provider-reported model artifact details. + [JsonPropertyName("details")] + public ModelArtifactDetails Details { get => field ??= new(); set; } - /// Authentication host. - [JsonPropertyName("host")] - public required string Host { get; set; } + /// Provider-reported artifact digest. + [JsonPropertyName("digest")] + public string? Digest { get; set; } - /// OAuth user login. - [JsonPropertyName("login")] - public required string Login { get; set; } + /// Provider-native model identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Provider-reported last-modified timestamp. + [JsonPropertyName("modifiedAt")] + public DateTimeOffset? ModifiedAt { 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; } } -/// Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. -/// The gh-cli variant of . +/// 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)] -public partial class SettableAuthInfoGhCli : SettableAuthInfo +public sealed class DiscoveredModelList { - /// - [JsonIgnore] - public override string Type => "gh-cli"; + /// Provider-native models in provider order. + [JsonPropertyName("models")] + public IList Models { get => field ??= []; 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; } - - /// User login as reported by `gh auth status`. - [JsonPropertyName("login")] - public required string Login { get; set; } - - /// The token returned by `gh auth token`. Treat as a secret. - [JsonPropertyName("token")] - public required string Token { get; set; } -} - -/// Authentication-info input variant for API-key authentication to a non-GitHub LLM provider, carrying the secret `apiKey` and host. -/// The api-key variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SettableAuthInfoApiKey : SettableAuthInfo -{ - /// - [JsonIgnore] - public override string Type => "api-key"; - - /// The API key. Treat as a secret. - [JsonPropertyName("apiKey")] - public required string ApiKey { get; set; } - - /// Snapshot of the authenticated user's Copilot subscription info, if known. Mirrors the GitHub API `/copilot_internal/v2/token` user response shape — the runtime trusts this verbatim and does not re-fetch when set. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("copilotUser")] - public CopilotUserResponse? CopilotUser { get; set; } - - /// Authentication host. - [JsonPropertyName("host")] - public required string Host { get; set; } + /// Typed operation outcome. + [JsonPropertyName("outcome")] + public ModelProviderOperationOutcome Outcome { get => field ??= new(); set; } } -/// New auth credentials to install on the session. Omit to leave credentials unchanged. +/// Provider model inventory request parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSetCredentialsParams +internal sealed class ModelProviderModelsListRequest { - /// 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; } + /// 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; } -/// Credential-free authentication identity safe to expose to hosts and user interfaces. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AuthIdentity -{ - /// Snapshot of the authenticated user's Copilot subscription info, if known. - [JsonPropertyName("copilotUser")] - public CopilotUserResponse? CopilotUser { get; set; } - - /// Name of the environment variable that supplied the credential, when applicable. - [JsonPropertyName("envVar")] - public string? EnvVar { get; set; } - - /// Authentication host. - [JsonPropertyName("host")] - public string Host { get; set; } = string.Empty; - - /// Authenticated login, when available. - [JsonPropertyName("login")] - public string? Login { get; set; } - - /// Opaque SDK GitHub credential registration backing this identity. Routing metadata only; never a credential. - [JsonPropertyName("registrationId")] - public string? RegistrationId { get; set; } - - /// Authentication type. - [JsonPropertyName("type")] - public AuthInfoType Type { get; set; } -} - -/// Identifies the target session. +/// Vision-specific limits. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionGitHubAuthGetCurrentAuthInfoRequest +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; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionGitHubAuthGetAllAuthAvailableRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Maximum number of images per prompt. + [JsonPropertyName("max_prompt_images")] + public long? MaxPromptImages { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionGitHubAuthRefreshCopilotUserRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } + public override string Action => "preserve"; } -/// Enumerate request carrying the typed collection query. +/// JSON union data type for ProtocolSectionOverride. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AccountsEnumerateRequest +[JsonConverter(typeof(Converter))] +public sealed partial class ProtocolSectionOverride { - /// Which typed accounts collection to enumerate. - [JsonPropertyName("query")] - public AuthEnumerateQuery Query { get => field ??= new(); set; } + /// Gets the value when this instance contains . + public ProtocolStaticSectionOverride? ProtocolStaticSectionOverride { get; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Gets the value when this instance contains . + public ProtocolMarkerSectionOverride? ProtocolMarkerSectionOverride { get; } -/// 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; -} + /// Initializes a new instance of the class from . + public ProtocolSectionOverride(ProtocolStaticSectionOverride value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolStaticSectionOverride = value; + } + /// Converts to . + public static implicit operator ProtocolSectionOverride(ProtocolStaticSectionOverride value) => new(value); -/// The activeAccount variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthReadValueActiveAccount : AuthReadValue -{ - /// - [JsonIgnore] - public override string Kind => "activeAccount"; + /// Initializes a new instance of the class from . + public ProtocolSectionOverride(ProtocolMarkerSectionOverride value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolMarkerSectionOverride = value; + } - /// The active account, or absent when not logged in. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("account")] - public AccountStatus? Account { get; set; } -} + /// Converts to . + public static implicit operator ProtocolSectionOverride(ProtocolMarkerSectionOverride value) => new(value); -/// 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; } + /// 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."); + } - /// Active account host, if authenticated. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("activeHost")] - public string? ActiveHost { 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); + } - /// Active account login, if authenticated. - [JsonPropertyName("activeLogin")] - public string? ActiveLogin { get; set; } + throw new JsonException("JSON value did not match any ProtocolSectionOverride variant."); + } - /// Copilot plan tier of the active account, if known. - [JsonPropertyName("copilotPlan")] - public string? CopilotPlan { 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; + } - /// Whether the session has resolved authentication. - [JsonPropertyName("isAuthenticated")] - public bool IsAuthenticated { get; set; } + throw new JsonException("No ProtocolSectionOverride variant value is set."); + } + } } -/// The status variant of . +/// RPC data type for ProtocolSystemMessageCustomizeConfig operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthReadValueStatus : AuthReadValue +public sealed class ProtocolSystemMessageCustomizeConfig { - /// - [JsonIgnore] - public override string Kind => "status"; + /// Text appended after the customized sections. + [JsonPropertyName("content")] + public string? Content { get; set; } - /// The neutral authentication status summary. - [JsonPropertyName("status")] - public required AuthStatusDto Status { get; set; } + /// Customize-mode discriminator. + [JsonPropertyName("mode")] + public ProtocolCustomizeMode Mode { get; set; } + + /// Named standard-prompt section overrides. + [JsonPropertyName("sections")] + public IDictionary? Sections { get; set; } } -/// The lastErrors variant of . +/// JSON union data type for ProtocolSystemMessageConfig. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthReadValueLastErrors : AuthReadValue +[JsonConverter(typeof(Converter))] +public sealed partial class ProtocolSystemMessageConfig { - /// - [JsonIgnore] - public override string Kind => "lastErrors"; + /// Gets the value when this instance contains . + public ProtocolSystemMessageAppendConfig? ProtocolSystemMessageAppendConfig { get; } - /// Validation errors from the most recent authentication attempt. - [JsonPropertyName("errors")] - public required IList Errors { get; set; } -} + /// Gets the value when this instance contains . + public ProtocolSystemMessageReplaceConfig? ProtocolSystemMessageReplaceConfig { get; } -/// Selects which typed accounts datum to read. -/// Polymorphic base type discriminated by kind. -[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 -{ - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; -} + /// Gets the value when this instance contains . + public ProtocolSystemMessageCustomizeConfig? ProtocolSystemMessageCustomizeConfig { get; } + /// Initializes a new instance of the class from . + public ProtocolSystemMessageConfig(ProtocolSystemMessageAppendConfig value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolSystemMessageAppendConfig = value; + } -/// The activeAccount variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthReadQueryActiveAccount : AuthReadQuery -{ - /// - [JsonIgnore] - public override string Kind => "activeAccount"; + /// Converts to . + public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageAppendConfig value) => new(value); + + /// Initializes a new instance of the class from . + public ProtocolSystemMessageConfig(ProtocolSystemMessageReplaceConfig value) + { + ArgumentNullException.ThrowIfNull(value); + ProtocolSystemMessageReplaceConfig = value; + } + + /// 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; + } + + /// Converts to . + public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageCustomizeConfig value) => new(value); + + /// 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."); + } + + 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); + } + + 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 status variant of . +/// A BYOK model definition referencing a named provider. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthReadQueryStatus : AuthReadQuery +public sealed class ProviderModelConfig { - /// - [JsonIgnore] - public override string Kind => "status"; + /// 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; + + /// Maximum context window tokens for the model. + [JsonPropertyName("maxContextWindowTokens")] + public double? MaxContextWindowTokens { get; set; } + + /// Maximum output tokens for the model. + [JsonPropertyName("maxOutputTokens")] + public double? MaxOutputTokens { get; set; } + + /// Maximum prompt/input tokens for the model. + [JsonPropertyName("maxPromptTokens")] + public double? MaxPromptTokens { get; set; } + + /// 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; } } -/// The lastErrors variant of . +/// Azure-specific provider options. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthReadQueryLastErrors : AuthReadQuery +public sealed class ProviderConfigAzure { - /// - [JsonIgnore] - public override string Kind => "lastErrors"; + /// API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. + [JsonPropertyName("apiVersion")] + public string? ApiVersion { get; set; } } -/// Read request carrying the typed datum query. +/// External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AccountsGetRequest +public sealed class NamedProviderConfig { - /// Which typed accounts datum to read. - [JsonPropertyName("query")] - public AuthReadQuery Query { get => field ??= new(); set; } + /// Static API key used to authenticate provider requests. + [JsonPropertyName("apiKey")] + public string? ApiKey { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Azure authentication configuration for the provider. + [JsonPropertyName("azure")] + public ProviderConfigAzure? Azure { get; set; } -/// 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; } + /// Base URL for provider API requests. + [JsonPropertyName("baseUrl")] + public string BaseUrl { get; set; } = string.Empty; - /// Whether the mutation was applied. - [JsonPropertyName("ok")] - public bool Ok { 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; } } -/// One non-interactive accounts mutation command (the selector is fused with its typed args). -/// Polymorphic base type discriminated by kind. +/// 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)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(AuthWriteSwitchActive), "switchActive")] -[JsonDerivedType(typeof(AuthWriteLogout), "logout")] -[JsonDerivedType(typeof(AuthWriteSetCredentials), "setCredentials")] -public partial class AuthWrite +public sealed class ModelProviderConfigurationPlan { - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; -} + /// 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; } -/// The switchActive variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthWriteSwitchActive : AuthWrite -{ - /// - [JsonIgnore] - public override string Kind => "switchActive"; + /// 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; } - /// Opaque selection id of the account to make active. + /// 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 required string SelectionId { get; set; } + 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 logout 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 AuthWriteLogout : AuthWrite +internal sealed class ModelProviderPrepareConfigurationRequest { - /// - [JsonIgnore] - public override string Kind => "logout"; + /// The discovered instance that serves the model. + [JsonPropertyName("instance")] + public ModelProviderInstance Instance { get => field ??= new(); 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; } + /// 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; } -/// The setCredentials variant of . +/// Managed sandbox enforcement state for a session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthWriteSetCredentials : AuthWrite +public sealed class SandboxEnforcementStatus { - /// - [JsonIgnore] - public override string Kind => "setCredentials"; - - /// Authentication host URL. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("host")] - public required string Host { get; set; } + /// Whether an enforcement failure has permanently blocked the session. + [JsonPropertyName("blocked")] + public bool Blocked { get; set; } - /// Login/username for the credential. - [JsonPropertyName("login")] - public required string Login { get; set; } + /// The first sandbox enforcement failure that blocked the session. + [JsonPropertyName("reason")] + public string? Reason { get; set; } - /// GitHub authentication token to install. - [JsonPropertyName("token")] - public required string Token { get; set; } + /// Whether the effective managed policy requires an available sandbox backend. + [JsonPropertyName("required")] + public bool Required { get; set; } } -/// Mutation request carrying the typed write command. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AccountsSetRequest +internal sealed class SessionSandboxGetEnforcementStatusRequest { - /// 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; } -/// 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. +/// Result of attempting to disable sandboxing for the current session. [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 SandboxDisableForSessionResult { - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; -} + /// The authoritative sandbox enabled state after the operation. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + /// Whether this call resolved the pending request and applied the session opt-out. + [JsonPropertyName("success")] + public bool Success { get; set; } +} -/// The open-url variant of . +/// 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 partial class AuthLoginStepOpenUrl : AuthLoginStep +public sealed class PermissionDecisionContext { - /// - [JsonIgnore] - public override string Kind => "open-url"; + /// Disposition of the permission request as observed by the responding client. + [JsonPropertyName("outcome")] + public PermissionDecisionOutcome Outcome { get; set; } - /// Authorize URL the consumer should open in a browser (consumer-driven browser-open). - [JsonPropertyName("url")] - public required string Url { get; set; } + /// Whether the responding client could ask a user interactively, was running headlessly, or had no response path. Omit when the client cannot determine this authoritatively. + [JsonPropertyName("responseCapability")] + public PermissionResponseCapability? ResponseCapability { get; set; } + + /// Controlled reason or actor responsible for the response. + [JsonPropertyName("source")] + public PermissionDecisionSource Source { get; set; } + + /// Client surface that submitted the response. + [JsonPropertyName("surface")] + public PermissionDecisionSurface Surface { get; set; } } -/// The input-required variant of . +/// Request to disable sandboxing for the current session while resolving an active sandbox-bypass permission prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthLoginStepInputRequired : AuthLoginStep +internal sealed class SandboxDisableForSessionRequest { - /// - [JsonIgnore] - public override string Kind => "input-required"; + /// Optional attribution for the permission decision. + [JsonPropertyName("decisionContext")] + public PermissionDecisionContext? DecisionContext { 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; } + /// 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; } -/// The awaiting variant of . +/// Result of accepting a sandbox path grant. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthLoginStepAwaiting : AuthLoginStep +public sealed class SandboxGrantPathForRequestResult { - /// - [JsonIgnore] - public override string Kind => "awaiting"; + /// Whether this call resolved the pending request and added the path to the session's sandbox policy. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// The needs-interaction variant of . +/// Request to accept the sandbox path grant offered on an active sandbox escalation permission prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthLoginStepNeedsInteraction : AuthLoginStep +internal sealed class SandboxGrantPathForRequestRequest { - /// - [JsonIgnore] - public override string Kind => "needs-interaction"; + /// 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; } -/// Terminal result of an interactive login flow. +/// Authentication status and account metadata for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AuthLoginResultDto +public sealed class SessionAuthStatus { - /// Host that was signed in, when completed. + /// 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; } - /// Login that was signed in, when completed. + /// 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; } - /// Terminal disposition of the login. - [JsonPropertyName("status")] - public AuthLoginResultStatus Status { get; set; } + /// Human-readable authentication status description. + [JsonPropertyName("statusMessage")] + public string? StatusMessage { get; set; } } -/// The completed variant of . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthLoginStepCompleted : AuthLoginStep +internal sealed class SessionGitHubAuthGetStatusRequest { - /// - [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 . +/// Indicates whether the credential update succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class AuthLoginStepError : AuthLoginStep +public sealed class SessionSetCredentialsResult { - /// - [JsonIgnore] - public override string Kind => "error"; + /// 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; } - /// Human-readable failure message. - [JsonPropertyName("message")] - public required string Message { get; set; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// A started login flow: its opaque id and first step. +/// 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)] -public sealed class AuthLoginBegun +[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(SettableAuthInfoAccount), "account")] +[JsonDerivedType(typeof(SettableAuthInfoGhCli), "gh-cli")] +[JsonDerivedType(typeof(SettableAuthInfoApiKey), "api-key")] +public partial class SettableAuthInfo { - /// Opaque flow id used to advance or cancel this login. - [JsonPropertyName("flowId")] - public string FlowId { get; set; } = string.Empty; - - /// The first step of the flow. - [JsonPropertyName("step")] - public AuthLoginStep Step { get => field ??= new(); set; } + /// The type discriminator. + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; } -/// Begin an interactive login flow for a provider kind. Dispatch is kind-only. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AuthLoginBeginRequest -{ - /// The provider kind to sign in with. - [JsonPropertyName("kind")] - public LoginProviderKind Kind { get; set; } - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} -/// Advance an in-flight login flow, optionally fulfilling an input-required step. +/// 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)] -internal sealed class AuthLoginAdvanceRequest +public partial class SettableAuthInfoHmac : SettableAuthInfo { - /// Opaque flow id from begin. - [JsonPropertyName("flowId")] - public string FlowId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Type => "hmac"; - /// Neutral input fulfilling a preceding input-required step (e.g. a GHEC host); ignored otherwise. - [JsonPropertyName("input")] - public string? Input { 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; + /// 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; } } -/// Cancel an in-flight login flow. +/// 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)] -internal sealed class AuthLoginCancelRequest -{ - /// Opaque flow id from begin. - [JsonPropertyName("flowId")] - public string FlowId { get; set; } = string.Empty; - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} - -/// A file included in the session debug bundle. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DebugCollectLogsCollectedEntry +public partial class SettableAuthInfoEnv : SettableAuthInfo { - /// Relative path of the file in the staged bundle/archive. - [JsonPropertyName("bundlePath")] - public string BundlePath { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Type => "env"; - /// Redacted output size in bytes. - [JsonPropertyName("sizeBytes")] - public long SizeBytes { 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; } - /// Source category for this entry. - [JsonPropertyName("source")] - public DebugCollectLogsSource Source { get; set; } -} + /// Name of the environment variable the token was sourced from. + [JsonPropertyName("envVar")] + public required string EnvVar { 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; + /// Authentication host (e.g. https://github.com or a GHES host). + [JsonPropertyName("host")] + public required string Host { get; set; } - /// Server-local source path that could not be read. - [JsonPropertyName("path")] - public string? Path { 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; } - /// Reason the entry was skipped. - [JsonPropertyName("reason")] - public string Reason { get; set; } = string.Empty; + /// The token value itself. Treat as a secret. + [JsonPropertyName("token")] + public required string Token { get; set; } } -/// Result of collecting a session debug bundle. +/// Token authentication accepted by session.gitHubAuth.setCredentials. +/// The token variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DebugCollectLogsResult +public partial class SettableAuthInfoToken : SettableAuthInfo { - /// Files included in the bundle. - [JsonPropertyName("entries")] - public IList Entries { get => field ??= []; set; } + /// + [JsonIgnore] + public override string Type => "token"; - /// Destination kind that was written. - [JsonPropertyName("kind")] - public DebugCollectLogsResultKind Kind { 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; } - /// 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; + /// Authentication host. + [JsonPropertyName("host")] + public required string Host { get; set; } - /// Optional files or directories that could not be included. - [JsonPropertyName("skippedEntries")] - public IList? SkippedEntries { get; set; } + /// The token value itself. Treat as a secret. + [JsonPropertyName("token")] + public required string Token { get; set; } } -/// A caller-provided server-local file or directory to include in the debug bundle. +/// 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)] -public sealed class DebugCollectLogsEntry +public partial class SettableAuthInfoCopilotApiToken : SettableAuthInfo { - /// 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; } + /// + [JsonIgnore] + public override string Type => "copilot-api-token"; - /// 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; } -} + /// 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; } -/// Destination for the session debug bundle. -/// Polymorphic base type discriminated by kind. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "kind", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(DebugCollectLogsDestinationArchive), "archive")] -[JsonDerivedType(typeof(DebugCollectLogsDestinationDirectory), "directory")] -public partial class DebugCollectLogsDestination -{ - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + /// Authentication host (always the public GitHub host). + [JsonPropertyName("host")] + public required string Host { get; set; } } - -/// The archive variant of . +/// 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 partial class DebugCollectLogsDestinationArchive : DebugCollectLogsDestination +public partial class SettableAuthInfoUser : SettableAuthInfo { /// [JsonIgnore] - public override string Kind => "archive"; + public override string Type => "user"; - /// 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; } + /// Authentication host. + [JsonPropertyName("host")] + public required string Host { get; set; } + + /// OAuth user login. + [JsonPropertyName("login")] + public required string Login { get; set; } } -/// The directory variant of . +/// 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 DebugCollectLogsDestinationDirectory : DebugCollectLogsDestination +public partial class SettableAuthInfoAccount : SettableAuthInfo { /// [JsonIgnore] - public override string Kind => "directory"; + public override string Type => "account"; - /// Directory where files should be staged. The directory is created if needed. - [JsonPropertyName("outputDirectory")] - public required string OutputDirectory { get; set; } + /// 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; } } -/// Built-in session diagnostics to include in the bundle. Omitted fields default to true. +/// 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 sealed class DebugCollectLogsInclude +public partial class SettableAuthInfoGhCli : SettableAuthInfo { - /// 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; } - - /// Include the session event log (`events.jsonl`). Defaults to true. - [JsonPropertyName("events")] - public bool? Events { 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; } + /// + [JsonIgnore] + public override string Type => "gh-cli"; - /// Maximum number of previous process logs to include. Defaults to 5. - [JsonPropertyName("previousProcessLogLimit")] - public long? PreviousProcessLogLimit { 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; } - /// 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; } + /// Authentication host. + [JsonPropertyName("host")] + public required string Host { get; set; } - /// Include process logs for the session. Defaults to true. - [JsonPropertyName("processLogs")] - public bool? ProcessLogs { get; set; } + /// User login as reported by `gh auth status`. + [JsonPropertyName("login")] + public required string Login { get; set; } - /// Include interactive shell logs written under the session's `shell-logs` directory. Defaults to true. - [JsonPropertyName("shellLogs")] - public bool? ShellLogs { get; set; } + /// The token returned by `gh auth token`. Treat as a secret. + [JsonPropertyName("token")] + public required string Token { get; set; } } -/// Options for collecting a session debug bundle with configurable redaction. +/// 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)] -internal sealed class DebugCollectLogsRequest +public partial class SettableAuthInfoApiKey : 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; } + /// + [JsonIgnore] + public override string Type => "api-key"; - /// 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; } + /// The API key. Treat as a secret. + [JsonPropertyName("apiKey")] + public required string ApiKey { get; set; } - /// 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. + [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. +/// New auth credentials to install on the session. Omit to leave credentials unchanged. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasAction +internal sealed class SessionSetCredentialsParams { - /// Description of the action. - [JsonPropertyName("description")] - public string? Description { get; set; } - - /// JSON Schema for the action input. - [JsonPropertyName("inputSchema")] - public JsonElement? InputSchema { get; set; } + /// 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; } - /// Action name exposed by the canvas provider. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Canvas available in the current session. +/// Credential-free authentication identity safe to expose to hosts and user interfaces. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DiscoveredCanvas +public sealed class AuthIdentity { - /// Actions the agent or host may invoke on an open instance. - [JsonPropertyName("actions")] - public IList? Actions { get; set; } - - /// Provider-local canvas identifier. - [JsonPropertyName("canvasId")] - public string CanvasId { 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; - - /// Human-readable canvas name. - [JsonPropertyName("displayName")] - public string DisplayName { get; set; } = string.Empty; + /// Snapshot of the authenticated user's Copilot subscription info, if known. + [JsonPropertyName("copilotUser")] + public CopilotUserResponse? CopilotUser { get; set; } - /// Owning provider identifier. - [JsonPropertyName("extensionId")] - public string ExtensionId { get; set; } = string.Empty; + /// Name of the environment variable that supplied the credential, when applicable. + [JsonPropertyName("envVar")] + public string? EnvVar { get; set; } - /// Owning extension display name, when available. - [JsonPropertyName("extensionName")] - public string? ExtensionName { get; set; } + /// Authentication host. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; - /// Host-local PNG path for the canvas icon, when supplied. - [JsonPropertyName("icon")] - public string? Icon { get; set; } + /// Authenticated login, when available. + [JsonPropertyName("login")] + public string? Login { get; set; } - /// JSON Schema for canvas open input. - [JsonPropertyName("inputSchema")] - public JsonElement? InputSchema { get; set; } -} + /// Opaque SDK GitHub credential registration backing this identity. Routing metadata only; never a credential. + [JsonPropertyName("registrationId")] + public string? RegistrationId { get; set; } -/// 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; } + /// Authentication type. + [JsonPropertyName("type")] + public AuthInfoType Type { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionCanvasListRequest +internal sealed class SessionGitHubAuthGetCurrentAuthInfoRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Open canvas instance snapshot. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class OpenCanvasInstance -{ - /// 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; } - - /// Input supplied when the instance was opened. - [JsonPropertyName("input")] - public JsonElement? Input { get; set; } - - /// 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; } -} - -/// Live open-canvas snapshot. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasListOpenResult +internal sealed class SessionGitHubAuthGetAllAuthAvailableRequest { - /// 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 SessionGitHubAuthRefreshCopilotUserRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Canvas open parameters. +/// Internal GitHub login parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CanvasOpenRequest +internal sealed class SessionAuthLoginRequest { - /// Provider-local canvas identifier. - [JsonPropertyName("canvasId")] - public string CanvasId { 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; } + /// 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; } - - /// Workflow run identifier. - [JsonPropertyName("runId")] - public required string RunId { get; set; } - - /// Suggested larger ceiling when the runtime can derive one safely. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("suggestedValue")] - public double? SuggestedValue { 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; } - /// Approved effective ceiling that was reached. - [JsonPropertyName("value")] - public required double Value { get; set; } -} + /// Authentication host URL. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; -/// 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"; + /// The provider kind of this account. + [JsonPropertyName("kind")] + public AccountKind Kind { get; set; } - /// Human-readable reason the resume did not proceed. - [JsonPropertyName("reason")] - public required string Reason { get; set; } + /// Authenticated login/username. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; - /// Workflow run identifier whose changed limits were declined. - [JsonPropertyName("runId")] - public required string RunId { get; set; } + /// Opaque selection id used to switch to, or log out, this account. + [JsonPropertyName("selectionId")] + public string SelectionId { get; set; } = string.Empty; } -/// The workflow_durable_failure variant of . +/// The accounts variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowRunFailureWorkflowDurableFailure : WorkflowRunFailure +public partial class AuthEnumerateValueAccounts : AuthEnumerateValue { /// [JsonIgnore] - public override string Type => "workflow_durable_failure"; - - /// Stable failure code. - [JsonPropertyName("code")] - public required string Code { get; set; } - - /// Execution-critical durable operation that failed. - [JsonPropertyName("operation")] - public required WorkflowDurableOperation Operation { get; set; } + public override string Kind => "accounts"; - /// Workflow run identifier. - [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 run stopped because its usage accounting could not be completed. -/// The workflow_accounting_incomplete variant of . +/// A provider offered for interactive login. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowRunFailureWorkflowAccountingIncomplete : WorkflowRunFailure +public sealed class ProviderDescriptor { - /// - [JsonIgnore] - public override string Type => "workflow_accounting_incomplete"; + /// Whether this provider is currently available to sign in with. + [JsonPropertyName("available")] + public bool Available { get; set; } - /// Confirmed usage in nano-AIU, representing the floor of what the run spent. - [JsonPropertyName("drainedNanoAiu")] - public required long DrainedNanoAiu { 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 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 providers variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowRunFailureWorkflowProviderDisconnected : WorkflowRunFailure +public partial class AuthEnumerateValueProviders : AuthEnumerateValue { /// [JsonIgnore] - public override string Type => "workflow_provider_disconnected"; + public override string Kind => "providers"; - /// Workflow run identifier. - [JsonPropertyName("runId")] - public required string RunId { get; set; } + /// The providers offered for interactive login. + [JsonPropertyName("items")] + public required IList Items { get; set; } } -/// Durable metadata describing who initiated a workflow pause. -/// Polymorphic base type discriminated by type. +/// 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 = "type", + TypeDiscriminatorPropertyName = "kind", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(WorkflowPauseInfoUser), "user")] -[JsonDerivedType(typeof(WorkflowPauseInfoCheckpoint), "checkpoint")] -public partial class WorkflowPauseInfo +[JsonDerivedType(typeof(AuthEnumerateQueryAccounts), "accounts")] +[JsonDerivedType(typeof(AuthEnumerateQueryProviders), "providers")] +public partial class AuthEnumerateQuery { /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// The user variant of . +/// The accounts variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowPauseInfoUser : WorkflowPauseInfo +public partial class AuthEnumerateQueryAccounts : AuthEnumerateQuery { /// [JsonIgnore] - public override string Type => "user"; + public override string Kind => "accounts"; } -/// The checkpoint variant of . +/// The providers variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class WorkflowPauseInfoCheckpoint : WorkflowPauseInfo +public partial class AuthEnumerateQueryProviders : AuthEnumerateQuery { /// [JsonIgnore] - public override string Type => "checkpoint"; + public override string Kind => "providers"; - /// Stable author-defined checkpoint key that initiated the pause. - [JsonPropertyName("key")] - public required string Key { get; set; } + /// 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; } } -/// Complete current or terminal workflow run envelope. +/// Enumerate request carrying the typed collection query. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunResult +internal sealed class AccountsEnumerateRequest { - /// 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; } - - /// Structured pause initiator metadata for a paused attempt. - [JsonPropertyName("pauseInfo")] - public WorkflowPauseInfo? PauseInfo { get; set; } + /// Which typed accounts collection to enumerate. + [JsonPropertyName("query")] + public AuthEnumerateQuery Query { get => field ??= new(); set; } - /// Reason for a halted or cancelled run. - [JsonPropertyName("reason")] - public string? Reason { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Completed workflow result. - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } +/// 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; +} - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; - /// Partial journal and progress snapshot for a halted, cancelled, or errored run. - [JsonPropertyName("snapshot")] - public JsonElement? Snapshot { get; set; } +/// The activeAccount variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthReadValueActiveAccount : AuthReadValue +{ + /// + [JsonIgnore] + public override string Kind => "activeAccount"; - /// Current or terminal workflow run status. - [JsonPropertyName("status")] - public WorkflowRunStatus Status { get; set; } + /// The active account, or absent when not logged in. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("account")] + public AccountStatus? Account { get; set; } } -/// Wire-only per-invocation workflow resource ceiling overrides. +/// Neutral authentication status summary. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunLimits +public sealed class AuthStatusDto { - /// 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; } + /// Number of signed-in accounts in the roster. + [JsonPropertyName("accountCount")] + public long AccountCount { get; set; } - /// Maximum number of workflow subagents that may run concurrently. - [JsonPropertyName("maxConcurrentSubagents")] - public long? MaxConcurrentSubagents { get; set; } + /// Active account host, if authenticated. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("activeHost")] + public string? ActiveHost { get; set; } - /// Maximum total number of workflow subagents that may be admitted. - [JsonPropertyName("maxTotalSubagents")] - public long? MaxTotalSubagents { get; set; } + /// Active account login, if authenticated. + [JsonPropertyName("activeLogin")] + public string? ActiveLogin { 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; } + /// Copilot plan tier of the active account, if known. + [JsonPropertyName("copilotPlan")] + public string? CopilotPlan { get; set; } + + /// Whether the session has resolved authentication. + [JsonPropertyName("isAuthenticated")] + public bool IsAuthenticated { get; set; } } -/// Options controlling workflow invocation. +/// The status variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunOptions +public partial class AuthReadValueStatus : AuthReadValue { - /// Per-invocation resource ceiling overrides. - [JsonPropertyName("limits")] - public WorkflowRunLimits? Limits { get; set; } + /// + [JsonIgnore] + public override string Kind => "status"; - /// Whether to emit workflow phase names to the session transcript. - [JsonPropertyName("logPhaseNames")] - public bool? LogPhaseNames { get; set; } + /// The neutral authentication status summary. + [JsonPropertyName("status")] + public required AuthStatusDto Status { get; set; } +} - /// Whether to notify the originating session when the workflow completes. - [JsonPropertyName("notifyOnComplete")] - public bool? NotifyOnComplete { get; set; } +/// The lastErrors variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthReadValueLastErrors : AuthReadValue +{ + /// + [JsonIgnore] + public override string Kind => "lastErrors"; - /// Run identifier whose journal and progress should seed this resumed run. - [JsonPropertyName("resumeFromRunId")] - public string? ResumeFromRunId { get; set; } + /// Validation errors from the most recent authentication attempt. + [JsonPropertyName("errors")] + public required IList Errors { get; set; } } -/// Parameters for invoking a registered workflow. +/// Selects which typed accounts datum to read. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowRunRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(AuthReadQueryActiveAccount), "activeAccount")] +[JsonDerivedType(typeof(AuthReadQueryStatus), "status")] +[JsonDerivedType(typeof(AuthReadQueryLastErrors), "lastErrors")] +public partial class AuthReadQuery { - /// Workflow input value. - [JsonPropertyName("args")] - public JsonElement Args { get; set; } - - /// Registered workflow name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { 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; +/// The activeAccount variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthReadQueryActiveAccount : AuthReadQuery +{ + /// + [JsonIgnore] + public override string Kind => "activeAccount"; } -/// Resolved persisted workflow identity and resumed run envelope. +/// The status variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowResumeResult +public partial class AuthReadQueryStatus : AuthReadQuery { - /// Terminal resumed run envelope. - [JsonPropertyName("run")] - public WorkflowRunResult Run { get => field ??= new(); set; } - - /// Persisted workflow name resolved for the resumed run. - [JsonPropertyName("workflowName")] - public string WorkflowName { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "status"; } -/// Parameters for resuming a workflow run from its persisted identity. +/// The lastErrors variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowResumeRequest +public partial class AuthReadQueryLastErrors : AuthReadQuery { - /// 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; } - - /// Whether to notify the originating session when the workflow completes. - [JsonPropertyName("notifyOnComplete")] - public bool? NotifyOnComplete { get; set; } + /// + [JsonIgnore] + public override string Kind => "lastErrors"; +} - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; +/// 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; } -/// Options for an internal tool-originated workflow invocation. +/// Result of a non-interactive accounts mutation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowToolRunOptions +public sealed class AuthWriteResult { - /// Per-invocation resource ceiling overrides. - [JsonPropertyName("limits")] - public WorkflowRunLimits? Limits { get; set; } + /// For a logout, whether other signed-in accounts remain. + [JsonPropertyName("moreUsers")] + public bool? MoreUsers { get; set; } - /// Run identifier whose journal and progress should seed this resumed run. - [JsonPropertyName("resumeFromRunId")] - public string? ResumeFromRunId { get; set; } + /// Whether the mutation was applied. + [JsonPropertyName("ok")] + public bool Ok { get; set; } } -/// Internal parameters for invoking a registered workflow from a tool. +/// 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 WorkflowToolRunRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(AuthWriteSwitchActive), "switchActive")] +[JsonDerivedType(typeof(AuthWriteLogout), "logout")] +[JsonDerivedType(typeof(AuthWriteSetCredentials), "setCredentials")] +public partial class AuthWrite { - /// Workflow input value. - [JsonPropertyName("args")] - public JsonElement Args { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { 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; } +/// 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; } +} - /// Opaque identifier of the originating tool call. - [JsonPropertyName("toolCallId")] - public string? ToolCallId { get; set; } +/// The logout variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthWriteLogout : AuthWrite +{ + /// + [JsonIgnore] + public override string Kind => "logout"; + + /// 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 resuming a workflow run from a tool. +/// The setCredentials variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowToolResumeRequest +public partial class AuthWriteSetCredentials : AuthWrite { - /// Optional per-invocation resource ceiling overrides. - [JsonPropertyName("limits")] - public WorkflowRunLimits? Limits { get; set; } + /// + [JsonIgnore] + public override string Kind => "setCredentials"; - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Authentication host URL. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("host")] + public required string Host { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Login/username for the credential. + [JsonPropertyName("login")] + public required string Login { get; set; } - /// Opaque identifier of the originating tool call. - [JsonPropertyName("toolCallId")] - public string? ToolCallId { get; set; } + /// GitHub authentication token to install. + [JsonPropertyName("token")] + public required string Token { get; set; } } -/// Parameters for retrieving a workflow run. +/// Mutation request carrying the typed write command. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowGetRunRequest +internal sealed class AccountsSetRequest { - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// 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; } -/// Declared or approved workflow resource ceilings. +/// 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)] -public sealed class WorkflowDeclaredLimits +[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 { - /// Maximum AI credits consumed by subagents and descendants. - [JsonPropertyName("maxAiCredits")] - public double? MaxAiCredits { get; set; } - - /// Maximum concurrently active subagents. - [JsonPropertyName("maxConcurrentSubagents")] - public long? MaxConcurrentSubagents { get; set; } - - /// Maximum total subagents spawned by the run. - [JsonPropertyName("maxTotalSubagents")] - public long? MaxTotalSubagents { get; set; } - - /// Maximum accumulated active execution time in seconds. - [JsonPropertyName("timeoutSeconds")] - public double? TimeoutSeconds { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Durable workflow resource consumption. + +/// The open-url variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunConsumed +public partial class AuthLoginStepOpenUrl : AuthLoginStep { - /// Accumulated active execution time in milliseconds. - [JsonPropertyName("activeMs")] - public long ActiveMs { get; set; } - - /// AI usage consumed by the run in nano-AIU. - [JsonPropertyName("nanoAiu")] - public long NanoAiu { get; set; } + /// + [JsonIgnore] + public override string Kind => "open-url"; - /// Total subagents spawned by the run. - [JsonPropertyName("subagents")] - public long Subagents { get; set; } + /// Authorize URL the consumer should open in a browser (consumer-driven browser-open). + [JsonPropertyName("url")] + public required string Url { get; set; } } -/// Current workflow phase identity. +/// The input-required variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowCurrentPhase +public partial class AuthLoginStepInputRequired : AuthLoginStep { - /// Current phase identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "input-required"; - /// Zero-based declared phase ordinal, or null for an undeclared phase. - [JsonPropertyName("ordinal")] - public long? Ordinal { 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; } } -/// Prompt-safe terminal workflow outcome. +/// The awaiting variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunTerminal +public partial class AuthLoginStepAwaiting : AuthLoginStep { - /// Human-readable terminal error. - [JsonPropertyName("error")] - public string? Error { get; set; } - - /// Machine-readable terminal failure. - [JsonPropertyName("failure")] - public WorkflowRunFailure? Failure { get; set; } - - /// Pause initiator metadata, or null when the run did not pause. - [JsonPropertyName("pauseInfo")] - public WorkflowPauseInfo? PauseInfo { get; set; } - - /// Human-readable terminal reason. - [JsonPropertyName("reason")] - public string? Reason { get; set; } - - /// Prompt-safe preview of the completed result. - [JsonPropertyName("resultPreview")] - public string? ResultPreview { get; set; } + /// + [JsonIgnore] + public override string Kind => "awaiting"; } -/// Durable workflow run summary with read-time live overlays. +/// The needs-interaction variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowRunSummary +public partial class AuthLoginStepNeedsInteraction : AuthLoginStep { - /// Epoch milliseconds when the current active segment started, or null while inactive. - [JsonPropertyName("activeSegmentStartedAt")] - public long? ActiveSegmentStartedAt { 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; } - - /// Epoch milliseconds when the run was created. - [JsonPropertyName("createdAt")] - public long CreatedAt { get; set; } - - /// Current phase identity, or null before any phase is entered. - [JsonPropertyName("currentPhase")] - public WorkflowCurrentPhase? CurrentPhase { get; set; } - - /// Resource ceilings declared by the workflow. - [JsonPropertyName("declaredLimits")] - public WorkflowDeclaredLimits DeclaredLimits { get => field ??= new(); set; } + /// + [JsonIgnore] + public override string Kind => "needs-interaction"; +} - /// Number of phases declared by the workflow. - [JsonPropertyName("declaredPhaseCount")] - public long DeclaredPhaseCount { get; set; } +/// A credential-free account choice after sign-in. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class AuthLoginAccount +{ + /// Host coordinate owned by the selected account's provider. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; - /// Human-readable workflow description. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; + /// Provider kind that owns this account choice. + [JsonPropertyName("kind")] + public AccountKind Kind { get; set; } - /// Number of direct workflow agents currently live. - [JsonPropertyName("liveAgentCount")] - public long LiveAgentCount { get; set; } + /// Human-readable login for the account choice. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; - /// Epoch milliseconds when this live-overlay snapshot was observed. - [JsonPropertyName("observedAt")] - public long ObservedAt { get; set; } + /// Opaque identifier supplied to the next login step to select this account. + [JsonPropertyName("selectionId")] + public string SelectionId { get; set; } = string.Empty; +} - /// Monotonic durable run revision. - [JsonPropertyName("revision")] - public long Revision { get; set; } +/// Result of an interactive login flow. Pending consent or account selection is not terminal. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class AuthLoginResultDto +{ + /// Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 first. + [JsonPropertyName("accounts")] + public IList? Accounts { get; set; } - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Host that was signed in, when completed. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("host")] + public string? Host { get; set; } - /// Epoch milliseconds when execution first started, or null before start. - [JsonPropertyName("startedAt")] - public long? StartedAt { get; set; } + /// Login that was signed in, when completed. + [JsonPropertyName("login")] + public string? Login { get; set; } - /// Current workflow run status. + /// Current disposition of the login, including pending user decisions. [JsonPropertyName("status")] - public WorkflowRunStatus Status { get; set; } + public AuthLoginResultStatus Status { get; set; } +} - /// Terminal run outcome, or null while nonterminal. - [JsonPropertyName("terminal")] - public WorkflowRunTerminal? Terminal { get; set; } +/// The completed variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthLoginStepCompleted : AuthLoginStep +{ + /// + [JsonIgnore] + public override string Kind => "completed"; - /// Total direct workflow agents spawned across all attempts. - [JsonPropertyName("totalSpawnedAgentCount")] - public long TotalSpawnedAgentCount { 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; } +} - /// Epoch milliseconds when the durable run was last updated. - [JsonPropertyName("updatedAt")] - public long UpdatedAt { get; set; } +/// The error variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class AuthLoginStepError : AuthLoginStep +{ + /// + [JsonIgnore] + public override string Kind => "error"; - /// Registered workflow name. - [JsonPropertyName("workflowName")] - public string WorkflowName { get; set; } = string.Empty; + /// Human-readable failure message. + [JsonPropertyName("message")] + public required string Message { get; set; } } -/// A page of workflow runs in durable creation order. +/// A started login flow: its opaque id and first step. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowListRunsResult +public sealed class AuthLoginBegun { - /// Whether terminal runs newer than this page exist. - [JsonPropertyName("hasMoreNewer")] - public bool? HasMoreNewer { get; set; } - - /// Newest terminal-run cursor in this page, or null when the terminal window is empty. - [JsonPropertyName("newestSeq")] - public long? NewestSeq { get; set; } + /// Opaque flow id used to advance or cancel this login. + [JsonPropertyName("flowId")] + public string FlowId { get; set; } = string.Empty; - /// Oldest terminal-run cursor in this page, or null when the terminal window is empty. - [JsonPropertyName("oldestSeq")] - public long? OldestSeq { get; set; } + /// The first step of the flow. + [JsonPropertyName("step")] + public AuthLoginStep Step { get => field ??= new(); set; } +} - /// Number of terminal runs older than this page. - [JsonPropertyName("omittedOlder")] - public long? OmittedOlder { get; set; } +/// Begin an interactive login flow for a provider kind. Dispatch is kind-only. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class AuthLoginBeginRequest +{ + /// The provider kind to sign in with. + [JsonPropertyName("kind")] + public LoginProviderKind Kind { get; set; } - /// Workflow run summaries in durable creation order. - [JsonPropertyName("runs")] - public IList Runs { get => field ??= []; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Parameters for paging workflow runs. +/// Advance an in-flight login flow, optionally fulfilling an input-required step. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowListRunsRequest +internal sealed class AuthLoginAdvanceRequest { - /// Exclusive forward cursor. - [JsonPropertyName("afterSeq")] - public long? AfterSeq { get; set; } - - /// Exclusive backward cursor. - [JsonPropertyName("beforeSeq")] - public long? BeforeSeq { get; set; } + /// Opaque flow id from begin. + [JsonPropertyName("flowId")] + public string FlowId { get; set; } = string.Empty; - /// Maximum terminal runs to return. Defaults to 200 and is capped at 500. - [JsonPropertyName("limit")] - public int? Limit { get; set; } + /// 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; } -/// Prompt-safe durable identity and live status for a direct workflow agent. +/// Cancel an in-flight login flow. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowAgentSummary +internal sealed class AuthLoginCancelRequest { - /// 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; } - - /// Stable direct-agent identifier. - [JsonPropertyName("agentId")] - public string AgentId { get; set; } = string.Empty; + /// Opaque flow id from begin. + [JsonPropertyName("flowId")] + public string FlowId { get; set; } = string.Empty; - /// Registered agent type. - [JsonPropertyName("agentType")] - public string AgentType { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Epoch milliseconds when the agent completed. - [JsonPropertyName("completedAt")] - public long? CompletedAt { 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; - /// Friendly, non-unique name intended for display. - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } + /// Redacted output size in bytes. + [JsonPropertyName("sizeBytes")] + public long SizeBytes { get; set; } - /// Friendly, non-unique name intended for display. - [JsonPropertyName("label")] - public string Label { get; set; } = string.Empty; + /// Source category for this entry. + [JsonPropertyName("source")] + public DebugCollectLogsSource Source { get; set; } +} - /// Phase identifier active when the agent was launched, or null. - [JsonPropertyName("phaseId")] - public string? PhaseId { 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; - /// Model requested when the agent was launched. - [JsonPropertyName("requestedModel")] - public string? RequestedModel { get; set; } + /// Server-local source path that could not be read. + [JsonPropertyName("path")] + public string? Path { get; set; } - /// Concrete model resolved for the agent. - [JsonPropertyName("resolvedModel")] - public string? ResolvedModel { get; set; } + /// Reason the entry was skipped. + [JsonPropertyName("reason")] + public string Reason { get; set; } = string.Empty; +} - /// Owning workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; +/// 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; } - /// Epoch milliseconds when the agent started. - [JsonPropertyName("startedAt")] - public long? StartedAt { get; set; } + /// Destination kind that was written. + [JsonPropertyName("kind")] + public DebugCollectLogsResultKind Kind { get; set; } - /// Current durable or live agent status. - [JsonPropertyName("status")] - public string Status { get; set; } = string.Empty; + /// 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; - /// Tool-call identifier that launched the agent. - [JsonPropertyName("toolCallId")] - public string ToolCallId { get; set; } = string.Empty; + /// Optional files or directories that could not be included. + [JsonPropertyName("skippedEntries")] + public IList? SkippedEntries { get; set; } } -/// Durable lifecycle and timing for one workflow phase. +/// A caller-provided server-local file or directory to include in the debug bundle. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowPhaseObservation +public sealed class DebugCollectLogsEntry { - /// Completed active time accumulated by this phase in milliseconds. - [JsonPropertyName("accumulatedActiveMs")] - public long AccumulatedActiveMs { 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; } + /// Relative path to use inside the staged bundle/archive. + [JsonPropertyName("bundlePath")] + public string BundlePath { get; set; } = string.Empty; - /// Current live active time for this phase in milliseconds. - [JsonPropertyName("currentActiveMs")] - public long CurrentActiveMs { get; set; } + /// Kind of source path to include. + [JsonPropertyName("kind")] + public DebugCollectLogsEntryKind Kind { get; set; } - /// Optional human-readable phase detail. - [JsonPropertyName("detail")] - public string? Detail { get; set; } + /// Server-local source path to read. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Number of times execution entered this phase. - [JsonPropertyName("entryCount")] - public long EntryCount { 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; } - /// Phase identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// 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; } +} - /// Most recent run attempt that entered this phase, or `0` if the phase has never been entered. - [JsonPropertyName("lastEnteredRunAttempt")] - public long LastEnteredRunAttempt { get; set; } +/// Destination for the session debug bundle. +/// Polymorphic base type discriminated by kind. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(DebugCollectLogsDestinationArchive), "archive")] +[JsonDerivedType(typeof(DebugCollectLogsDestinationDirectory), "directory")] +public partial class DebugCollectLogsDestination +{ + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} - /// Direct agents in this phase that are currently live. - [JsonPropertyName("liveAgentCount")] - public long LiveAgentCount { get; set; } - /// Zero-based declared phase ordinal, or null for an undeclared phase. - [JsonPropertyName("ordinal")] - public long? Ordinal { get; set; } +/// The archive variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class DebugCollectLogsDestinationArchive : DebugCollectLogsDestination +{ + /// + [JsonIgnore] + public override string Kind => "archive"; - /// 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; } + /// 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; } - /// Derived lifecycle state of the phase. - [JsonPropertyName("status")] - public WorkflowPhaseStatus Status { get; set; } + /// Absolute or server-relative path for the .tgz archive to create. + [JsonPropertyName("outputPath")] + public required string OutputPath { get; set; } +} - /// Human-readable phase title. - [JsonPropertyName("title")] - public string Title { get; set; } = string.Empty; +/// The directory variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class DebugCollectLogsDestinationDirectory : DebugCollectLogsDestination +{ + /// + [JsonIgnore] + public override string Kind => "directory"; - /// Total direct agents associated with this phase. - [JsonPropertyName("totalAgentCount")] - public long TotalAgentCount { get; set; } + /// Directory where files should be staged. The directory is created if needed. + [JsonPropertyName("outputDirectory")] + public required string OutputDirectory { get; set; } } -/// One durable workflow progress record. +/// Built-in session diagnostics to include in the bundle. Omitted fields default to true. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowProgressLine +public sealed class DebugCollectLogsInclude { - /// Resume attempt that emitted this record. - [JsonPropertyName("attempt")] - public long Attempt { get; set; } + /// 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; } - /// Progress record kind. - [JsonPropertyName("kind")] - public WorkflowLogLineKind Kind { get; set; } + /// Include the session event log (`events.jsonl`). Defaults to true. + [JsonPropertyName("events")] + public bool? Events { get; set; } - /// Phase active when the record was emitted, or null before any phase. - [JsonPropertyName("phaseId")] - public string? PhaseId { 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; } - /// Epoch milliseconds when the record was persisted. - [JsonPropertyName("recordedAt")] - public long RecordedAt { get; set; } + /// Maximum number of previous process logs to include. Defaults to 5. + [JsonPropertyName("previousProcessLogLimit")] + public long? PreviousProcessLogLimit { get; set; } - /// Global monotonic sequence number within the run. - [JsonPropertyName("seq")] - public long Seq { 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; } - /// Prompt-safe progress text. - [JsonPropertyName("text")] - public string Text { get; set; } = string.Empty; + /// Include process logs for the session. Defaults to true. + [JsonPropertyName("processLogs")] + public bool? ProcessLogs { get; set; } + + /// Include interactive shell logs written under the session's `shell-logs` directory. Defaults to true. + [JsonPropertyName("shellLogs")] + public bool? ShellLogs { get; set; } } -/// A bidirectional page of workflow progress. +/// Options for collecting a session debug bundle with configurable redaction. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowProgressPage +internal sealed class DebugCollectLogsRequest { - /// Whether progress records newer than this page exist. - [JsonPropertyName("hasMoreNewer")] - public bool HasMoreNewer { get; set; } - - /// Whether progress records older than this page exist. - [JsonPropertyName("hasMoreOlder")] - public bool HasMoreOlder { get; set; } - - /// Newest sequence number in this page, or null when empty. - [JsonPropertyName("newestSeq")] - public long? NewestSeq { get; set; } + /// 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; } - /// Oldest sequence number in this page, or null when empty. - [JsonPropertyName("oldestSeq")] - public long? OldestSeq { 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; } - /// Progress records in sequence order. - [JsonPropertyName("records")] - public IList Records { get => field ??= []; set; } + /// Which built-in session diagnostics to include. Omitted fields default to true. + [JsonPropertyName("include")] + public DebugCollectLogsInclude? Include { get; set; } - /// Run revision reflected by this page. - [JsonPropertyName("revision")] - public long Revision { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Full workflow run observability detail. +/// 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 WorkflowRunDetail +public sealed class CanvasAction { - /// Epoch milliseconds when the current active segment started, or null while inactive. - [JsonPropertyName("activeSegmentStartedAt")] - public long? ActiveSegmentStartedAt { get; set; } + /// Description of the action. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Durable identities and live statuses for direct workflow agents. - [JsonPropertyName("agents")] - public IList Agents { get => field ??= []; set; } + /// JSON Schema for the action input. + [JsonPropertyName("inputSchema")] + public JsonElement? InputSchema { 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; } + /// Action name exposed by the canvas provider. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} - /// Epoch milliseconds when the run completed, or null while nonterminal. - [JsonPropertyName("completedAt")] - public long? CompletedAt { 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; } - /// Durable resource consumption. - [JsonPropertyName("consumed")] - public WorkflowRunConsumed Consumed { get => field ??= new(); set; } + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { get; set; } = string.Empty; - /// Epoch milliseconds when the run was created. - [JsonPropertyName("createdAt")] - public long CreatedAt { 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; - /// Current phase identity, or null before any phase is entered. - [JsonPropertyName("currentPhase")] - public WorkflowCurrentPhase? CurrentPhase { get; set; } + /// Human-readable canvas name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; - /// Resource ceilings declared by the workflow. - [JsonPropertyName("declaredLimits")] - public WorkflowDeclaredLimits DeclaredLimits { get => field ??= new(); set; } + /// Owning provider identifier. + [JsonPropertyName("extensionId")] + public string ExtensionId { get; set; } = string.Empty; - /// Number of phases declared by the workflow. - [JsonPropertyName("declaredPhaseCount")] - public long DeclaredPhaseCount { get; set; } + /// Owning extension display name, when available. + [JsonPropertyName("extensionName")] + public string? ExtensionName { get; set; } - /// Human-readable workflow description. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; + /// Host-local PNG path for the canvas icon, when supplied. + [JsonPropertyName("icon")] + public string? Icon { get; set; } - /// Number of direct workflow agents currently live. - [JsonPropertyName("liveAgentCount")] - public long LiveAgentCount { get; set; } + /// JSON Schema for canvas open input. + [JsonPropertyName("inputSchema")] + public JsonElement? InputSchema { get; set; } +} - /// Epoch milliseconds when this live-overlay snapshot was observed. - [JsonPropertyName("observedAt")] - public long ObservedAt { get; set; } +/// 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; } +} - /// Lifecycle and timing observations for each workflow phase. - [JsonPropertyName("phases")] - public IList Phases { get => field ??= []; 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; +} - /// Bidirectional page of durable workflow progress. - [JsonPropertyName("progress")] - public WorkflowProgressPage Progress { get => field ??= new(); set; } +/// Open canvas instance snapshot. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class OpenCanvasInstance +{ + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { get; set; } = string.Empty; - /// Monotonic durable run revision. - [JsonPropertyName("revision")] - public long Revision { get; set; } + /// Owning provider identifier. + [JsonPropertyName("extensionId")] + public string ExtensionId { get; set; } = string.Empty; - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Owning extension display name, when available. + [JsonPropertyName("extensionName")] + public string? ExtensionName { get; set; } - /// Epoch milliseconds when execution first started, or null before start. - [JsonPropertyName("startedAt")] - public long? StartedAt { get; set; } + /// Host-local PNG path for the canvas icon, when supplied. + [JsonPropertyName("icon")] + public string? Icon { get; set; } - /// Current workflow run status. - [JsonPropertyName("status")] - public WorkflowRunStatus Status { get; set; } + /// Input supplied when the instance was opened. + [JsonPropertyName("input")] + public JsonElement? Input { get; set; } - /// Terminal run outcome, or null while nonterminal. - [JsonPropertyName("terminal")] - public WorkflowRunTerminal? Terminal { get; set; } + /// Stable caller-supplied canvas instance identifier. + [JsonPropertyName("instanceId")] + public string InstanceId { get; set; } = string.Empty; - /// Total direct workflow agents spawned across all attempts. - [JsonPropertyName("totalSpawnedAgentCount")] - public long TotalSpawnedAgentCount { get; set; } + /// Provider-supplied status text. + [JsonPropertyName("status")] + public string? Status { get; set; } - /// Epoch milliseconds when the durable run was last updated. - [JsonPropertyName("updatedAt")] - public long UpdatedAt { get; set; } + /// Rendered title. + [JsonPropertyName("title")] + public string? Title { get; set; } - /// Registered workflow name. - [JsonPropertyName("workflowName")] - public string WorkflowName { get; set; } = string.Empty; + /// URL for web-rendered canvases. + [JsonPropertyName("url")] + public string? Url { get; set; } } -/// Parameters for paging workflow progress. +/// Live open-canvas snapshot. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowGetRunProgressRequest +public sealed class CanvasListOpenResult { - /// 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; + /// Currently open canvas instances. + [JsonPropertyName("openCanvases")] + public IList OpenCanvases { get => field ??= []; 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; } -/// Parameters for cancelling a workflow run. +/// Canvas open parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowCancelRequest +internal sealed class CanvasOpenRequest { - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { 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; } + + /// Canvas open input. + [JsonPropertyName("input")] + public JsonElement? Input { 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; } -/// Parameters for pausing a running workflow. +/// Canvas close parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowPauseRequest +internal sealed class CanvasCloseRequest { - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// 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; } -/// RPC data type for SessionWorkflowPauseAtCheckpoint operations. +/// Canvas action invocation result. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionWorkflowPauseAtCheckpointResult +public sealed class CanvasActionInvokeResult { - /// Whether this execution attempt must pause or may continue. - [JsonPropertyName("action")] - public WorkflowPauseCheckpointAction Action { get; set; } + /// Provider-supplied action result. + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } } -/// Parameters for an owned durable pause checkpoint. +/// Canvas action invocation parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowPauseCheckpointRequest +internal sealed class CanvasActionInvokeRequest { - /// Opaque token identifying the execution attempt that reached the checkpoint. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; + /// Action name to invoke. + [JsonPropertyName("actionName")] + public string ActionName { get; set; } = string.Empty; - /// Stable author-defined checkpoint key. - [JsonPropertyName("key")] - public string Key { get; set; } = string.Empty; + /// Action input. + [JsonPropertyName("input")] + public JsonElement? Input { get; set; } - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// 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; } -/// Acknowledgement that a workflow request was accepted. +/// Internal canvas provider registration parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowAckResult +internal sealed class CanvasProviderRegisterRequest { -} + /// Canvas contributions supplied by the provider. + [JsonPropertyName("canvases")] + public IList Canvases { get => field ??= []; set; } -/// One ordered workflow progress line. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowLogLine -{ - /// Progress line kind. - [JsonPropertyName("kind")] - public WorkflowLogLineKind Kind { get; set; } + /// Connection identifier for callback routing. + [JsonPropertyName("connectionId")] + public string ConnectionId { get; set; } = string.Empty; - /// Monotonic sequence number within the workflow run. - [JsonPropertyName("seq")] - public long Seq { get; set; } + /// Provider metadata supplied by the host. + [JsonPropertyName("info")] + public JsonElement Info { get; set; } - /// Progress text. - [JsonPropertyName("text")] - public string Text { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Parameters for recording workflow progress. +/// Internal canvas provider unregistration parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowLogRequest +internal sealed class CanvasProviderUnregisterRequest { - /// 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; + /// Connection identifier to unregister. + [JsonPropertyName("connectionId")] + public string ConnectionId { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Result of one workflow-scoped subagent call. +/// Machine-readable workflow run failure. +/// Polymorphic base type discriminated by type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowAgentResult +[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 { - /// Agent result, omitted when the agent produced no result. - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } + /// The type discriminator. + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; } -/// Options for one workflow-scoped subagent call. + +/// The workflow_limit_reached variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowAgentOptions +public partial class WorkflowRunFailureWorkflowLimitReached : WorkflowRunFailure { - /// 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; } + /// + [JsonIgnore] + public override string Type => "workflow_limit_reached"; - /// Optional label distinguishing otherwise identical memoized agent calls. - [JsonPropertyName("label")] - public string? Label { get; set; } + /// Resource ceiling that stopped the run. + [JsonPropertyName("kind")] + public required WorkflowRunFailureKind Kind { get; set; } - /// Optional model identifier for the subagent. - [JsonPropertyName("model")] - public string? Model { get; set; } + /// Workflow run identifier. + [JsonPropertyName("runId")] + public required string RunId { get; set; } - /// Optional reasoning effort override for the subagent. - [JsonPropertyName("reasoningEffort")] - public string? ReasoningEffort { get; set; } + /// Suggested larger ceiling when the runtime can derive one safely. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("suggestedValue")] + public double? SuggestedValue { get; set; } - /// Optional JSON Schema for structured agent output. - [JsonPropertyName("schema")] - public JsonElement? Schema { get; set; } + /// Approved effective ceiling that was reached. + [JsonPropertyName("value")] + public required double Value { get; set; } } -/// Parameters for one workflow-scoped subagent call. +/// The workflow_resume_declined variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowAgentRequest +public partial class WorkflowRunFailureWorkflowResumeDeclined : WorkflowRunFailure { - /// Opaque token identifying the current workflow execution attempt. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; - - /// Subagent execution options. - [JsonPropertyName("opts")] - public WorkflowAgentOptions Opts { get => field ??= new(); set; } - - /// Prompt to send to the subagent. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Type => "workflow_resume_declined"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Human-readable reason the resume did not proceed. + [JsonPropertyName("reason")] + public required string Reason { get; set; } - /// Workflow run identifier that owns the subagent. - [JsonPropertyName("workflowRunId")] - public string WorkflowRunId { get; set; } = string.Empty; + /// Workflow run identifier whose changed limits were declined. + [JsonPropertyName("runId")] + public required string RunId { get; set; } } -/// Result of reading a workflow journal entry. +/// The workflow_durable_failure variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowJournalGetResult +public partial class WorkflowRunFailureWorkflowDurableFailure : WorkflowRunFailure { - /// Whether the journal contained the requested key. - [JsonPropertyName("hit")] - public bool Hit { get; set; } - - /// Cached JSON result. The hit field distinguishes a cached JSON null from a miss. - [JsonPropertyName("resultJson")] - public JsonElement? ResultJson { get; set; } -} + /// + [JsonIgnore] + public override string Type => "workflow_durable_failure"; -/// Parameters for reading a workflow journal entry. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkflowJournalGetRequest -{ - /// Opaque token identifying the current workflow execution attempt. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; + /// Stable failure code. + [JsonPropertyName("code")] + public required string Code { get; set; } - /// Namespaced journal key. - [JsonPropertyName("key")] - public string Key { get; set; } = string.Empty; + /// 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 storing a workflow journal entry. +/// 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 WorkflowJournalPutRequest +public partial class WorkflowRunFailureWorkflowAccountingIncomplete : WorkflowRunFailure { - /// 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; + /// + [JsonIgnore] + public override string Type => "workflow_accounting_incomplete"; - /// JSON result to memoize. - [JsonPropertyName("resultJson")] - public JsonElement ResultJson { get; set; } + /// 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; } } -/// 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. +/// 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)] -public sealed class CurrentModel +public partial class WorkflowRunFailureWorkflowProviderDisconnected : WorkflowRunFailure { - /// 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; } + /// + [JsonIgnore] + public override string Type => "workflow_provider_disconnected"; - /// 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 required string RunId { get; set; } } -/// Identifies the target session. +/// Durable metadata describing who initiated a workflow pause. +/// Polymorphic base type discriminated by type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionModelGetCurrentRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "type", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(WorkflowPauseInfoUser), "user")] +[JsonDerivedType(typeof(WorkflowPauseInfoCheckpoint), "checkpoint")] +public partial class WorkflowPauseInfo { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// The type discriminator. + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; } -/// RPC data type for ModelSwitchConfirmation operations. + +/// The user variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelSwitchConfirmation +public partial class WorkflowPauseInfoUser : WorkflowPauseInfo { - /// 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; } - - /// Display name of the model that requires compaction confirmation. - [JsonPropertyName("targetModelDisplayName")] - public string TargetModelDisplayName { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Type => "user"; } -/// The model identifier active on the session after the switch. +/// The checkpoint variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelSwitchToResult +public partial class WorkflowPauseInfoCheckpoint : WorkflowPauseInfo { - /// 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; } - - /// 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; } + /// + [JsonIgnore] + public override string Type => "checkpoint"; - /// User-facing warning produced while applying the model switch. - [JsonPropertyName("warning")] - public string? Warning { get; set; } + /// Stable author-defined checkpoint key that initiated the pause. + [JsonPropertyName("key")] + public required string Key { get; set; } } -/// Vision-specific limits. +/// Complete current or terminal workflow run envelope. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelCapabilitiesOverrideLimitsVision +public sealed class WorkflowRunResult { - /// Maximum image size in bytes. - [JsonPropertyName("max_prompt_image_size")] - public long? MaxPromptImageSize { 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; } - /// Maximum number of images per prompt. - [JsonPropertyName("max_prompt_images")] - public long? MaxPromptImages { get; set; } + /// Error message for an errored run. + [JsonPropertyName("error")] + public string? Error { get; set; } - /// MIME types the model accepts. - [JsonPropertyName("supported_media_types")] - public IList? SupportedMediaTypes { get; set; } -} + /// Machine-readable failure details for a halted or errored run. + [JsonPropertyName("failure")] + public WorkflowRunFailure? Failure { get; set; } -/// Token limits for prompts, outputs, and context window. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelCapabilitiesOverrideLimits -{ - /// Maximum total context window size in tokens. - [JsonPropertyName("max_context_window_tokens")] - public long? MaxContextWindowTokens { get; set; } + /// Structured pause initiator metadata for a paused attempt. + [JsonPropertyName("pauseInfo")] + public WorkflowPauseInfo? PauseInfo { get; set; } - /// Maximum number of output/completion tokens. - [JsonPropertyName("max_output_tokens")] - public long? MaxOutputTokens { get; set; } + /// Reason for a halted or cancelled run. + [JsonPropertyName("reason")] + public string? Reason { get; set; } - /// Maximum number of prompt/input tokens. - [JsonPropertyName("max_prompt_tokens")] - public long? MaxPromptTokens { get; set; } + /// Completed workflow result. + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } - /// Vision-specific limits. - [JsonPropertyName("vision")] - public ModelCapabilitiesOverrideLimitsVision? Vision { get; set; } + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { 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; } } -/// Feature flags indicating what the model supports. +/// Wire-only per-invocation workflow resource ceiling overrides. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelCapabilitiesOverrideSupports +public sealed class WorkflowRunLimits { - /// 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; } + /// 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; } - /// Whether this model supports reasoning effort configuration. - [JsonPropertyName("reasoningEffort")] - public bool? ReasoningEffort { get; set; } + /// Maximum number of workflow subagents that may run concurrently. + [JsonPropertyName("maxConcurrentSubagents")] + public long? MaxConcurrentSubagents { get; set; } - /// Whether this model supports canonical tool calling. - [JsonPropertyName("toolCalls")] - public bool? ToolCalls { get; set; } + /// Maximum total number of workflow subagents that may be admitted. + [JsonPropertyName("maxTotalSubagents")] + public long? MaxTotalSubagents { get; set; } - /// Whether this model supports vision/image input. - [JsonPropertyName("vision")] - public bool? Vision { 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; } } -/// Optional capability overrides (vision, tool_calls, reasoning, etc.). +/// Options controlling workflow invocation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelCapabilitiesOverride +public sealed class WorkflowRunOptions { - /// Token limits for prompts, outputs, and context window. + /// Per-invocation resource ceiling overrides. [JsonPropertyName("limits")] - public ModelCapabilitiesOverrideLimits? Limits { get; set; } + public WorkflowRunLimits? Limits { get; set; } - /// Feature flags indicating what the model supports. - [JsonPropertyName("supports")] - public ModelCapabilitiesOverrideSupports? Supports { get; set; } -} + /// Whether to emit workflow phase names to the session transcript. + [JsonPropertyName("logPhaseNames")] + public bool? LogPhaseNames { get; set; } -/// Environment variables consulted while resolving model-picker settings. -public sealed class ModelPickerSettingsContextEnvironment -{ + /// 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; } } -/// Filesystem and environment context used to resolve model-picker settings. +/// Parameters for invoking a registered workflow. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelPickerSettingsContext +internal sealed class WorkflowRunRequest { - /// Optional Copilot configuration directory containing persisted settings. - [JsonPropertyName("configDir")] - public string? ConfigDir { get; set; } + /// Workflow input value. + [JsonPropertyName("args")] + public JsonElement Args { get; set; } - /// Environment variables consulted while resolving model-picker settings. - [JsonPropertyName("environment")] - public ModelPickerSettingsContextEnvironment Environment { get => field ??= new(); set; } + /// Registered workflow name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// User home directory used when resolving persisted settings. - [JsonPropertyName("homeDirectory")] - public string HomeDirectory { 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; } -/// RPC data type for ModelPickerPersistence operations. +/// Resolved persisted workflow identity and resumed run envelope. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelPickerPersistenceRequest +public sealed class WorkflowResumeResult { - /// Whether context tier was explicitly selected and should be persisted. - [JsonPropertyName("contextTierExplicit")] - public bool? ContextTierExplicit { get; set; } - - /// Whether reasoning effort was explicitly selected and should be persisted. - [JsonPropertyName("reasoningEffortExplicit")] - public bool? ReasoningEffortExplicit { get; set; } + /// Terminal resumed run envelope. + [JsonPropertyName("run")] + public WorkflowRunResult Run { get => field ??= new(); set; } - /// Filesystem and environment context used to resolve settings persistence. - [JsonPropertyName("settingsContext")] - public ModelPickerSettingsContext SettingsContext { get => field ??= new(); set; } + /// Persisted workflow name resolved for the resumed run. + [JsonPropertyName("workflowName")] + public string WorkflowName { get; set; } = string.Empty; } -/// Target model identifier and optional reasoning effort, summary, capability overrides, and context tier. +/// Parameters for resuming a workflow run from its persisted identity. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ModelSwitchToRequest +internal sealed class WorkflowResumeRequest { - /// 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; } - - /// 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; } - - /// 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; } - - /// 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; } - - /// Override individual model capabilities resolved by the runtime. - [JsonPropertyName("modelCapabilities")] - public ModelCapabilitiesOverride? ModelCapabilities { get; set; } - - /// Settings scope used when persisting the selected model. - [JsonPropertyName("modelChangeScope")] - public string? ModelChangeScope { get; 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; - - /// Optional settings context and explicit-override flags used to persist a picker selection. - [JsonPropertyName("pickerPersistence")] - public ModelPickerPersistenceRequest? PickerPersistence { 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; } - - /// Reasoning summary mode to request for supported model clients. - [JsonPropertyName("reasoningSummary")] - public ReasoningSummary? ReasoningSummary { get; set; } + /// Optional per-invocation resource ceiling overrides. + [JsonPropertyName("limits")] + public WorkflowRunLimits? Limits { get; set; } - /// Optional repository settings scope to persist after the switch commits. - [JsonPropertyName("repoScope")] - public string? RepoScope { get; set; } + /// Whether to emit workflow phase names to the session transcript. + [JsonPropertyName("logPhaseNames")] + public bool? LogPhaseNames { get; set; } - /// Require the target to be currently available and enabled before applying the switch. - [JsonPropertyName("requireAvailable")] - public bool? RequireAvailable { get; set; } + /// Whether to notify the originating session when the workflow completes. + [JsonPropertyName("notifyOnComplete")] + public bool? NotifyOnComplete { get; set; } - /// When true, evaluate context-window compaction policy before applying the switch. - [JsonPropertyName("runCompactionPreflight")] - public bool? RunCompactionPreflight { 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; - - /// 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; } - - /// Output verbosity level to request for supported models. - [JsonPropertyName("verbosity")] - public Verbosity? Verbosity { get; set; } } -/// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. +/// Options for an internal tool-originated workflow invocation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelSwitchAutoTierResult +internal sealed class WorkflowToolRunOptions { - /// 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. - [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; } + /// Per-invocation resource ceiling overrides. + [JsonPropertyName("limits")] + public WorkflowRunLimits? Limits { 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; } + /// Run identifier whose journal and progress should seed this resumed run. + [JsonPropertyName("resumeFromRunId")] + public string? ResumeFromRunId { get; set; } } -/// An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. +/// Internal parameters for invoking a registered workflow from a tool. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ModelSwitchAutoTierRequest +internal sealed class WorkflowToolRunRequest { - /// 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; } + /// Workflow input value. + [JsonPropertyName("args")] + public JsonElement Args { get; set; } + + /// 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; - /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. - [JsonPropertyName("source")] - public ModelChangeSource? Source { get; set; } + /// Opaque identifier of the originating tool call. + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } } -/// Managed, repository, and CLI model overrides to overlay onto the session at startup. +/// Internal parameters for resuming a workflow run from a tool. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ModelApplyStartupOverlayRequest +internal sealed class WorkflowToolResumeRequest { - /// 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; } + /// Optional per-invocation resource ceiling overrides. + [JsonPropertyName("limits")] + public WorkflowRunLimits? Limits { get; set; } - /// Model required by server-managed policy, when configured. - [JsonPropertyName("serverManagedModel")] - public string? ServerManagedModel { 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; -} - -/// The applied host allowlist and effective session model policy after intersection. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelSetAllowedModelsResult -{ - /// 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; } + /// Opaque identifier of the originating tool call. + [JsonPropertyName("toolCallId")] + public string? ToolCallId { 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. +/// Parameters for retrieving a workflow run. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ModelSetAllowedModelsRequest +internal sealed class WorkflowGetRunRequest { - /// Exact model IDs to permit, or null to clear the host restriction. - [JsonPropertyName("allowedModels")] - public IList? AllowedModels { 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; } -/// 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. +/// Declared or approved workflow resource ceilings. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelSetReasoningEffortResult +public sealed class WorkflowDeclaredLimits { - /// Reasoning effort level recorded on the session after the update. - [JsonPropertyName("reasoningEffort")] - public string ReasoningEffort { get; set; } = string.Empty; + /// Maximum AI credits consumed by subagents and descendants. + [JsonPropertyName("maxAiCredits")] + public double? MaxAiCredits { get; set; } + + /// Maximum concurrently active subagents. + [JsonPropertyName("maxConcurrentSubagents")] + public long? MaxConcurrentSubagents { get; set; } + + /// Maximum total subagents spawned by the run. + [JsonPropertyName("maxTotalSubagents")] + public long? MaxTotalSubagents { get; set; } + + /// Maximum accumulated active execution time in seconds. + [JsonPropertyName("timeoutSeconds")] + public double? TimeoutSeconds { get; set; } } -/// Reasoning effort level to apply to the currently selected model. +/// Durable workflow resource consumption. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ModelSetReasoningEffortRequest +public sealed class WorkflowRunConsumed { - /// 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; + /// Accumulated active execution time in milliseconds. + [JsonPropertyName("activeMs")] + public long ActiveMs { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// AI usage consumed by the run in nano-AIU. + [JsonPropertyName("nanoAiu")] + public long NanoAiu { get; set; } + + /// Total subagents spawned by the run. + [JsonPropertyName("subagents")] + public long Subagents { get; set; } } -/// Cost-category metadata for a CAPI model. +/// Current workflow phase identity. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionModelPriceCategory +public sealed class WorkflowCurrentPhase { - /// CAPI model identifier. + /// Current phase identifier. [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; - /// Cost category assigned to the model. - [JsonPropertyName("priceCategory")] - public ModelPickerPriceCategory PriceCategory { get; set; } + /// Zero-based declared phase ordinal, or null for an undeclared phase. + [JsonPropertyName("ordinal")] + public long? Ordinal { 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. +/// Prompt-safe terminal workflow outcome. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ModelProviderDescriptor +public sealed class WorkflowRunTerminal { - /// Opaque, stable provider id, stamped onto every model this provider returns. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Human-readable terminal error. + [JsonPropertyName("error")] + public string? Error { get; set; } - /// The neutral provider kind. - [JsonPropertyName("kind")] - public ModelProviderKind Kind { get; set; } + /// Machine-readable terminal failure. + [JsonPropertyName("failure")] + public WorkflowRunFailure? Failure { 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; + /// Pause initiator metadata, or null when the run did not pause. + [JsonPropertyName("pauseInfo")] + public WorkflowPauseInfo? PauseInfo { get; set; } - /// Stable ordering key for presenting providers in a deterministic sequence. - [JsonPropertyName("ordering")] - public long Ordering { get; set; } + /// Human-readable terminal reason. + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + /// Prompt-safe preview of the completed result. + [JsonPropertyName("resultPreview")] + public string? ResultPreview { get; set; } } -/// The list of models available to this session. +/// Durable workflow run summary with read-time live overlays. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionModelList +public sealed class WorkflowRunSummary { - /// 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; } + /// Epoch milliseconds when the current active segment started, or null while inactive. + [JsonPropertyName("activeSegmentStartedAt")] + public long? ActiveSegmentStartedAt { get; 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; } + /// Approved effective resource ceilings, or null until approved. + [JsonPropertyName("approved")] + public WorkflowDeclaredLimits? Approved { 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 the durable run state currently passes runtime resume eligibility checks. + [JsonPropertyName("canResume")] + public bool CanResume { get; set; } - /// Per-quota snapshots returned alongside the model list, keyed by quota type. - [JsonPropertyName("quotaSnapshots")] - public IDictionary? QuotaSnapshots { get; set; } -} + /// Epoch milliseconds when the run completed, or null while nonterminal. + [JsonPropertyName("completedAt")] + public long? CompletedAt { 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; } -} + /// Durable resource consumption. + [JsonPropertyName("consumed")] + public WorkflowRunConsumed Consumed { get => field ??= new(); 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; + /// Epoch milliseconds when the run was created. + [JsonPropertyName("createdAt")] + public long CreatedAt { get; set; } - /// If true, bypasses the per-session model list cache and re-fetches from CAPI. - [JsonPropertyName("skipCache")] - public bool? SkipCache { get; set; } -} + /// Current phase identity, or null before any phase is entered. + [JsonPropertyName("currentPhase")] + public WorkflowCurrentPhase? CurrentPhase { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionModeGetRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Resource ceilings declared by the workflow. + [JsonPropertyName("declaredLimits")] + public WorkflowDeclaredLimits DeclaredLimits { get => field ??= new(); 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; } + /// Number of phases declared by the workflow. + [JsonPropertyName("declaredPhaseCount")] + public long DeclaredPhaseCount { get; set; } - /// Compaction confirmation required before the mode change can complete. - [JsonPropertyName("confirmation")] - public ModelSwitchConfirmation? Confirmation { get; set; } + /// Human-readable workflow description. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; - /// Whether the host must defer implementing the requested mode change. - [JsonPropertyName("deferImplementation")] - public bool? DeferImplementation { get; set; } + /// Number of direct workflow agents currently live. + [JsonPropertyName("liveAgentCount")] + public long LiveAgentCount { get; set; } - /// Deprecation warnings associated with the model selected by the mode change. - [JsonPropertyName("deprecationWarnings")] - public IList? DeprecationWarnings { get; set; } + /// Epoch milliseconds when this live-overlay snapshot was observed. + [JsonPropertyName("observedAt")] + public long ObservedAt { get; set; } - /// User-facing outcome message for the model switch triggered by the mode change. - [JsonPropertyName("message")] - public string? Message { get; set; } + /// Monotonic durable run revision. + [JsonPropertyName("revision")] + public long Revision { 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; } + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; - /// Whether applying the mode changed the active model. - [JsonPropertyName("modelChanged")] - public bool ModelChanged { get; set; } + /// Epoch milliseconds when execution first started, or null before start. + [JsonPropertyName("startedAt")] + public long? StartedAt { get; set; } - /// Lifecycle status of the requested mode change. + /// Current workflow run status. [JsonPropertyName("status")] - public string Status { get; set; } = string.Empty; - - /// User-facing warning produced while applying the mode change. - [JsonPropertyName("warning")] - public string? Warning { get; set; } -} + public WorkflowRunStatus Status { 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; } + /// Terminal run outcome, or null while nonterminal. + [JsonPropertyName("terminal")] + public WorkflowRunTerminal? Terminal { 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; } + /// Total direct workflow agents spawned across all attempts. + [JsonPropertyName("totalSpawnedAgentCount")] + public long TotalSpawnedAgentCount { get; set; } - /// Session whose plan-mode base state should be inherited. - [JsonPropertyName("inheritPlanBaseFromSessionId")] - public string? InheritPlanBaseFromSessionId { get; set; } + /// Epoch milliseconds when the durable run was last updated. + [JsonPropertyName("updatedAt")] + public long UpdatedAt { get; set; } - /// The session mode the agent is operating in. - [JsonPropertyName("mode")] - public SessionMode Mode { get; set; } + /// Registered workflow name. + [JsonPropertyName("workflowName")] + public string WorkflowName { get; set; } = string.Empty; +} - /// Whether the selected plan model should be persisted. - [JsonPropertyName("persistPlanSelection")] - public bool? PersistPlanSelection { get; set; } +/// A page of workflow runs in durable creation order. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkflowListRunsResult +{ + /// Whether terminal runs newer than this page exist. + [JsonPropertyName("hasMoreNewer")] + public bool? HasMoreNewer { get; set; } - /// Settings context used when persisting the selected plan model. - [JsonPropertyName("pickerSettingsContext")] - public ModelPickerSettingsContext? PickerSettingsContext { get; set; } + /// Newest terminal-run cursor in this page, or null when the terminal window is empty. + [JsonPropertyName("newestSeq")] + public long? NewestSeq { get; set; } - /// Context tier to use with the dedicated plan model. - [JsonPropertyName("planContextTier")] - public string? PlanContextTier { get; set; } + /// Oldest terminal-run cursor in this page, or null when the terminal window is empty. + [JsonPropertyName("oldestSeq")] + public long? OldestSeq { get; set; } - /// Action to perform when leaving plan mode. - [JsonPropertyName("planExitAction")] - public string? PlanExitAction { get; set; } + /// Number of terminal runs older than this page. + [JsonPropertyName("omittedOlder")] + public long? OmittedOlder { get; set; } - /// Dedicated model to use in plan mode, when configured. - [JsonPropertyName("planModel")] - public string? PlanModel { get; set; } + /// Workflow run summaries in durable creation order. + [JsonPropertyName("runs")] + public IList Runs { get => field ??= []; set; } +} - /// Whether a dedicated plan model is configured. - [JsonPropertyName("planModelConfigured")] - public bool? PlanModelConfigured { get; set; } +/// Parameters for paging workflow runs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class WorkflowListRunsRequest +{ + /// Exclusive forward cursor. + [JsonPropertyName("afterSeq")] + public long? AfterSeq { get; set; } - /// Reasoning effort to use with the dedicated plan model. - [JsonPropertyName("planReasoningEffort")] - public string? PlanReasoningEffort { get; set; } + /// Exclusive backward cursor. + [JsonPropertyName("beforeSeq")] + public long? BeforeSeq { get; set; } - /// Whether leaving plan mode should restore the session's previous model. - [JsonPropertyName("restorePlanModel")] - public bool? RestorePlanModel { 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; } -/// The session's friendly name, or null when not yet set. +/// Prompt-safe durable identity and live status for a direct workflow agent. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class NameGetResult +public sealed class WorkflowAgentSummary { - /// The session name (user-set or auto-generated), or null if not yet set. - [JsonPropertyName("name")] - public string? Name { get; set; } -} + /// Accumulated active agent time in milliseconds. + [JsonPropertyName("activeMs")] + public long ActiveMs { get; set; } -/// 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; -} + /// Prompt-safe live activity text. + [JsonPropertyName("activity")] + public string? Activity { get; set; } -/// New friendly name to apply to the session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class NameSetRequest -{ - /// 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; + /// Stable direct-agent identifier. + [JsonPropertyName("agentId")] + public string AgentId { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Registered agent type. + [JsonPropertyName("agentType")] + public string AgentType { get; set; } = string.Empty; -/// Indicates whether the auto-generated summary was applied as the session's name. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class NameSetAutoResult -{ - /// 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; } -} + /// Epoch milliseconds when the agent completed. + [JsonPropertyName("completedAt")] + public long? CompletedAt { get; set; } -/// 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; + /// Friendly, non-unique name intended for display. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - /// Auto-generated session summary. Empty/whitespace-only values are ignored; values are trimmed before persisting. - [JsonPropertyName("summary")] - public string Summary { get; set; } = string.Empty; -} + /// Friendly, non-unique name intended for display. + [JsonPropertyName("label")] + public string Label { get; set; } = string.Empty; -/// Existence, contents, and resolved path of the session plan file. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PlanReadResult -{ - /// The content of the plan file, or null if it does not exist. - [JsonPropertyName("content")] - public string? Content { get; set; } + /// Phase identifier active when the agent was launched, or null. + [JsonPropertyName("phaseId")] + public string? PhaseId { get; set; } - /// Whether the plan file exists in the workspace. - [JsonPropertyName("exists")] - public bool Exists { get; set; } + /// Model requested when the agent was launched. + [JsonPropertyName("requestedModel")] + public string? RequestedModel { get; set; } - /// Absolute file path of the plan file, or null if workspace is not enabled. - [JsonPropertyName("path")] - public string? Path { get; set; } -} + /// Concrete model resolved for the agent. + [JsonPropertyName("resolvedModel")] + public string? ResolvedModel { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPlanReadRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Owning workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { 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; + /// 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; -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPlanDeleteRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Tool-call identifier that launched the agent. + [JsonPropertyName("toolCallId")] + public string ToolCallId { 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. +/// Durable lifecycle and timing for one workflow phase. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PlanSqlTodosRow +public sealed class WorkflowPhaseObservation { - /// 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; } + /// Completed active time accumulated by this phase in milliseconds. + [JsonPropertyName("accumulatedActiveMs")] + public long AccumulatedActiveMs { get; set; } - /// Todo description. - [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; } - /// Todo identifier. + /// Current live active time for this phase in milliseconds. + [JsonPropertyName("currentActiveMs")] + public long CurrentActiveMs { get; set; } + + /// Optional human-readable phase detail. + [JsonPropertyName("detail")] + public string? Detail { get; set; } + + /// Number of times execution entered this phase. + [JsonPropertyName("entryCount")] + public long EntryCount { get; set; } + + /// Phase identifier. [JsonPropertyName("id")] - public string? Id { get; set; } + public string Id { get; set; } = string.Empty; - /// Todo status. + /// Most recent run attempt that entered this phase, or `0` if the phase has never been entered. + [JsonPropertyName("lastEnteredRunAttempt")] + public long LastEnteredRunAttempt { get; set; } + + /// Direct agents in this phase that are currently live. + [JsonPropertyName("liveAgentCount")] + public long LiveAgentCount { get; set; } + + /// Zero-based declared phase ordinal, or null for an undeclared phase. + [JsonPropertyName("ordinal")] + public long? Ordinal { 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; } + + /// Derived lifecycle state of the phase. [JsonPropertyName("status")] - public string? Status { get; set; } + public WorkflowPhaseStatus Status { get; set; } - /// Todo title. + /// Human-readable phase title. [JsonPropertyName("title")] - public string? Title { get; set; } -} + public string Title { get; set; } = string.Empty; -/// Todo rows read from the session SQL database. Empty when no session database is available. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PlanReadSqlTodosResult -{ - /// 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; } + /// Total direct agents associated with this phase. + [JsonPropertyName("totalAgentCount")] + public long TotalAgentCount { get; set; } } -/// Identifies the target session. +/// One durable workflow progress record. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPlanReadSqlTodosRequest +public sealed class WorkflowProgressLine { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Resume attempt that emitted this record. + [JsonPropertyName("attempt")] + public long Attempt { 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; + /// Progress record kind. + [JsonPropertyName("kind")] + public WorkflowLogLineKind Kind { get; set; } - /// ID of the todo that has the dependency. - [JsonPropertyName("todoId")] - public string TodoId { get; set; } = string.Empty; + /// Phase active when the record was emitted, or null before any phase. + [JsonPropertyName("phaseId")] + public string? PhaseId { 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; } -/// Todo rows + dependency edges read from the session SQL database. +/// A bidirectional page of workflow progress. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PlanReadSqlTodosWithDependenciesResult +public sealed class WorkflowProgressPage { - /// 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; } + /// Whether progress records newer than this page exist. + [JsonPropertyName("hasMoreNewer")] + public bool HasMoreNewer { 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; } + /// Whether progress records older than this page exist. + [JsonPropertyName("hasMoreOlder")] + public bool HasMoreOlder { get; set; } + + /// Newest sequence number in this page, or null when empty. + [JsonPropertyName("newestSeq")] + public long? NewestSeq { get; set; } + + /// Oldest sequence number in this page, or null when empty. + [JsonPropertyName("oldestSeq")] + public long? OldestSeq { get; set; } + + /// Progress records in sequence order. + [JsonPropertyName("records")] + public IList Records { get => field ??= []; 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 SessionPlanReadSqlTodosWithDependenciesRequest +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; } -/// RPC data type for WorkspacesGetWorkspaceResultWorkspace operations. -public sealed class WorkspacesGetWorkspaceResultWorkspace -{ - /// Current Git branch. - [JsonPropertyName("branch")] - public string? Branch { get; set; } + /// Durable identities and live statuses for direct workflow agents. + [JsonPropertyName("agents")] + public IList Agents { get => field ??= []; set; } - /// Whether the per-session Chronicle upgrade prompt was dismissed for the workspace. - [JsonPropertyName("chronicle_sync_dismissed")] - public bool? ChronicleSyncDismissed { get; set; } + /// Approved effective resource ceilings, or null until approved. + [JsonPropertyName("approved")] + public WorkflowDeclaredLimits? Approved { get; set; } - /// Name of the client that created the workspace. - [JsonPropertyName("client_name")] - public string? ClientName { get; set; } + /// Whether the durable run state currently passes runtime resume eligibility checks. + [JsonPropertyName("canResume")] + public bool CanResume { get; set; } - /// Timestamp when the workspace was created. - [JsonPropertyName("created_at")] - public DateTimeOffset? CreatedAt { get; set; } + /// Epoch milliseconds when the run completed, or null while nonterminal. + [JsonPropertyName("completedAt")] + public long? CompletedAt { get; set; } - /// Current working directory associated with the workspace. - [JsonPropertyName("cwd")] - public string? Cwd { get; set; } + /// Durable resource consumption. + [JsonPropertyName("consumed")] + public WorkflowRunConsumed Consumed { get => field ??= new(); set; } - /// Git repository root associated with the workspace. - [JsonPropertyName("git_root")] - public string? GitRoot { get; set; } + /// Epoch milliseconds when the run was created. + [JsonPropertyName("createdAt")] + public long CreatedAt { get; set; } - /// Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. - [JsonPropertyName("host_type")] - public WorkspacesWorkspaceDetailsHostType? HostType { get; set; } + /// Current phase identity, or null before any phase is entered. + [JsonPropertyName("currentPhase")] + public WorkflowCurrentPhase? CurrentPhase { 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; + /// Resource ceilings declared by the workflow. + [JsonPropertyName("declaredLimits")] + public WorkflowDeclaredLimits DeclaredLimits { get => field ??= new(); set; } - /// Most recent Mission Control event identifier observed for the workspace. - [JsonPropertyName("mc_last_event_id")] - public string? McLastEventId { get; set; } + /// Number of phases declared by the workflow. + [JsonPropertyName("declaredPhaseCount")] + public long DeclaredPhaseCount { get; set; } - /// Mission Control session identifier associated with the workspace. - [JsonPropertyName("mc_session_id")] - public string? McSessionId { get; set; } + /// Human-readable workflow description. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; - /// Mission Control task identifier associated with the workspace. - [JsonPropertyName("mc_task_id")] - public string? McTaskId { get; set; } + /// Number of direct workflow agents currently live. + [JsonPropertyName("liveAgentCount")] + public long LiveAgentCount { get; set; } - /// Workspace display name. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// Epoch milliseconds when this live-overlay snapshot was observed. + [JsonPropertyName("observedAt")] + public long ObservedAt { get; set; } - /// Whether the workspace session can be steered remotely. - [JsonPropertyName("remote_steerable")] - public bool? RemoteSteerable { get; set; } + /// Lifecycle and timing observations for each workflow phase. + [JsonPropertyName("phases")] + public IList Phases { get => field ??= []; set; } - /// Repository identifier associated with the workspace. - [JsonPropertyName("repository")] - public string? Repository { get; set; } + /// Bidirectional page of durable workflow progress. + [JsonPropertyName("progress")] + public WorkflowProgressPage Progress { get => field ??= new(); set; } - /// Number of persisted summaries in the workspace. - [JsonPropertyName("summary_count")] - public long? SummaryCount { get; set; } + /// Monotonic durable run revision. + [JsonPropertyName("revision")] + public long Revision { get; set; } - /// Timestamp when the workspace was last updated. - [JsonPropertyName("updated_at")] - public DateTimeOffset? UpdatedAt { get; set; } + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; - /// Whether the workspace name was explicitly chosen by the user. - [JsonPropertyName("user_named")] - public bool? UserNamed { get; set; } -} + /// Epoch milliseconds when execution first started, or null before start. + [JsonPropertyName("startedAt")] + public long? StartedAt { get; set; } -/// Current workspace metadata for the session, including its absolute filesystem path when available. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesGetWorkspaceResult -{ - /// 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 workflow run status. + [JsonPropertyName("status")] + public WorkflowRunStatus Status { get; set; } - /// Current workspace metadata, or null if not available. - [JsonPropertyName("workspace")] - public WorkspacesGetWorkspaceResultWorkspace? Workspace { get; set; } + /// Terminal run outcome, or null while nonterminal. + [JsonPropertyName("terminal")] + public WorkflowRunTerminal? Terminal { get; set; } + + /// Total direct workflow agents spawned across all attempts. + [JsonPropertyName("totalSpawnedAgentCount")] + public long TotalSpawnedAgentCount { get; set; } + + /// Epoch milliseconds when the durable run was last updated. + [JsonPropertyName("updatedAt")] + public long UpdatedAt { get; set; } + + /// Registered workflow name. + [JsonPropertyName("workflowName")] + public string WorkflowName { get; set; } = string.Empty; } -/// Identifies the target session. +/// Parameters for paging workflow progress. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionWorkspacesGetWorkspaceRequest +internal sealed class WorkflowGetRunProgressRequest { + /// 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; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Workspace metadata fields to update. +/// Parameters for cancelling a workflow run. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesUpdateMetadataRequest +internal sealed class WorkflowCancelRequest { - /// Opaque workspace context supplied by the session host. - [JsonPropertyName("context")] - public JsonElement? Context { get; set; } - - /// 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 pausing a running workflow. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesEnsureRequest +internal sealed class WorkflowPauseRequest { - /// Opaque workspace context supplied by the session host. - [JsonPropertyName("context")] - public JsonElement? Context { 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. +/// RPC data type for SessionWorkflowPauseAtCheckpoint operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesListFilesResult +internal sealed class SessionWorkflowPauseAtCheckpointResult { - /// Slash-separated relative file paths in the workspace files directory. - [JsonPropertyName("files")] - public IList Files { get => field ??= []; 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 SessionWorkspacesListFilesRequest +internal sealed class WorkflowPauseCheckpointRequest { + /// 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; } -/// Contents of the requested workspace file as a UTF-8 string. +/// Acknowledgement that a workflow request was accepted. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesReadFileResult +public sealed class WorkflowAckResult { - /// File content as a UTF-8 string. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; } -/// Relative path of the workspace file to read. +/// One ordered workflow progress line. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesReadFileRequest +public sealed class WorkflowLogLine { - /// Slash-separated relative path within the workspace files directory. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Progress line kind. + [JsonPropertyName("kind")] + public WorkflowLogLineKind Kind { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Monotonic sequence number within the workflow run. + [JsonPropertyName("seq")] + public long Seq { get; set; } + + /// Progress text. + [JsonPropertyName("text")] + public string Text { get; set; } = string.Empty; } -/// Relative path and UTF-8 content for the workspace file to create or overwrite. +/// Parameters for recording workflow progress. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesCreateFileRequest +internal sealed class WorkflowLogRequest { - /// File content to write as a UTF-8 string. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// Opaque token identifying the current workflow execution attempt. + [JsonPropertyName("executionToken")] + public string ExecutionToken { get; set; } = string.Empty; - /// Slash-separated relative path within the workspace files directory. - [JsonPropertyName("path")] - public string Path { 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; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Filesystem metadata for a path in the session workspace files directory. +/// Result of one workflow-scoped subagent call. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesStatFileResult +public sealed class WorkflowAgentResult { - /// Creation time in Unix epoch milliseconds. - [JsonPropertyName("birthtimeMs")] - public double BirthtimeMs { get; set; } + /// Agent result, omitted when the agent produced no result. + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } +} - /// Whether the path identifies a directory. - [JsonPropertyName("isDirectory")] - public bool IsDirectory { get; set; } +/// Options for one workflow-scoped subagent call. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkflowAgentOptions +{ + /// Optional built-in or custom agent name whose definition configures the subagent. + [JsonPropertyName("agent")] + public string? Agent { get; set; } - /// Whether the path identifies a regular file. - [JsonPropertyName("isFile")] - public bool IsFile { get; set; } + /// Optional context tier override for the subagent. + [JsonPropertyName("contextTier")] + public ContextTier? ContextTier { get; set; } - /// Last modification time in Unix epoch milliseconds. - [JsonPropertyName("mtimeMs")] - public double MtimeMs { get; set; } + /// Optional label distinguishing otherwise identical memoized agent calls. + [JsonPropertyName("label")] + public string? Label { get; set; } - /// Size in bytes. - [JsonPropertyName("size")] - public double Size { get; set; } + /// Optional model identifier for the subagent. + [JsonPropertyName("model")] + public string? Model { get; set; } + + /// Optional reasoning effort override for the subagent. + [JsonPropertyName("reasoningEffort")] + public string? ReasoningEffort { get; set; } + + /// Optional JSON Schema for structured agent output. + [JsonPropertyName("schema")] + public JsonElement? Schema { get; set; } } -/// Relative path of the workspace file or directory to inspect. +/// Parameters for one workflow-scoped subagent call. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesStatFileRequest +internal sealed class WorkflowAgentRequest { - /// Slash-separated relative path within the workspace files directory. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Opaque token identifying the current workflow execution attempt. + [JsonPropertyName("executionToken")] + public string ExecutionToken { get; set; } = string.Empty; + + /// Subagent execution options. + [JsonPropertyName("opts")] + public WorkflowAgentOptions Opts { get => field ??= new(); set; } + + /// Prompt to send to the subagent. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; /// 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; } -/// Directory to create within the session workspace files directory. +/// Result of reading a workflow journal entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesCreateDirectoryRequest +public sealed class WorkflowJournalGetResult { - /// Slash-separated relative path within the workspace files directory. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - - /// Whether to create missing parent directories. Defaults to false. - [JsonPropertyName("recursive")] - public bool? Recursive { get; set; } + /// Whether the journal contained the requested key. + [JsonPropertyName("hit")] + public bool Hit { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Cached JSON result. The hit field distinguishes a cached JSON null from a miss. + [JsonPropertyName("resultJson")] + public JsonElement? ResultJson { get; set; } } -/// File or directory to remove from the session workspace files directory. +/// Parameters for reading a workflow journal entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesRemovePathRequest +internal sealed class WorkflowJournalGetRequest { - /// Whether a missing path should be treated as success. Defaults to false. - [JsonPropertyName("force")] - public bool? Force { get; set; } + /// Opaque token identifying the current workflow execution attempt. + [JsonPropertyName("executionToken")] + public string ExecutionToken { get; set; } = string.Empty; - /// Slash-separated relative path within the workspace files directory. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Namespaced journal key. + [JsonPropertyName("key")] + public string Key { get; set; } = string.Empty; - /// Whether to remove directory contents recursively. Defaults to false. - [JsonPropertyName("recursive")] - public bool? Recursive { 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; } -/// Source and destination paths for a rename within the session workspace files directory. +/// Parameters for storing a workflow journal entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesRenamePathRequest +internal sealed class WorkflowJournalPutRequest { - /// Slash-separated destination path relative to the workspace files directory. - [JsonPropertyName("destination")] - public string Destination { get; set; } = string.Empty; + /// 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; - - /// Slash-separated source path relative to the workspace files directory. - [JsonPropertyName("source")] - public string Source { get; set; } = string.Empty; } -/// Workspace checkpoint metadata with assigned number, human-readable title, and checkpoint filename. +/// 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 WorkspacesCheckpoints +public sealed class CurrentModel { - /// Filename of the checkpoint within the workspace checkpoints directory. - [JsonPropertyName("filename")] - public string Filename { get; set; } = string.Empty; + /// 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; } - /// Checkpoint number assigned by the workspace manager. - [JsonPropertyName("number")] - public long Number { 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; } - /// Human-readable checkpoint title. - [JsonPropertyName("title")] - public string Title { get; set; } = string.Empty; -} + /// Context tier for models that support multiple context-window sizes. + [JsonPropertyName("contextTier")] + public ContextTier? ContextTier { 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; } + /// 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 SessionWorkspacesListCheckpointsRequest +internal sealed class SessionModelGetCurrentRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Checkpoint content as a UTF-8 string, or null when the checkpoint or workspace is missing. +/// RPC data type for ModelSwitchConfirmation operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesReadCheckpointResult +public sealed class ModelSwitchConfirmation { - /// Checkpoint content as a UTF-8 string, or null when the checkpoint or workspace is missing. - [JsonPropertyName("content")] - public string? Content { get; set; } + /// 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; } + + /// Display name of the model that requires compaction confirmation. + [JsonPropertyName("targetModelDisplayName")] + public string TargetModelDisplayName { get; set; } = string.Empty; } -/// Checkpoint number to read. +/// The model identifier active on the session after the switch. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesReadCheckpointRequest +public sealed class ModelSwitchToResult { - /// Checkpoint number to read. - [JsonPropertyName("number")] - public long Number { get; set; } + /// Compaction confirmation projection when status is confirmation_required. + [JsonPropertyName("confirmation")] + public ModelSwitchConfirmation? Confirmation { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { 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; } -/// Metadata for the persisted summary. -public sealed class WorkspacesAddSummaryResultSummary -{ -} + /// Deprecation warnings associated with the selected model or options. + [JsonPropertyName("deprecationWarnings")] + public IList? DeprecationWarnings { get; set; } -/// Refreshed metadata for the containing workspace. -public sealed class WorkspacesAddSummaryResultWorkspace -{ -} + /// User-facing outcome message for the model switch. + [JsonPropertyName("message")] + public string? Message { get; set; } -/// Persisted summary metadata and refreshed workspace metadata. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesAddSummaryResult -{ - /// Metadata for the persisted summary. - [JsonPropertyName("summary")] - public WorkspacesAddSummaryResultSummary? Summary { get; set; } + /// Currently active model identifier after the switch. + [JsonPropertyName("modelId")] + public string? ModelId { get; set; } - /// Refreshed metadata for the containing workspace. - [JsonPropertyName("workspace")] - public WorkspacesAddSummaryResultWorkspace? Workspace { 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; } -/// 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; + /// Persistence failure encountered after applying the model switch. + [JsonPropertyName("persistenceError")] + public string? PersistenceError { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } - /// Summary title shown in checkpoint listings. - [JsonPropertyName("title")] - public string Title { get; set; } = string.Empty; + /// 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; } } -/// Rollback point for local workspace summaries. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesTruncateSummariesRequest +/// Environment variables consulted while resolving model-picker settings. +public sealed class ModelPickerSettingsContextEnvironment { - /// Number of newest summaries to keep. - [JsonPropertyName("keepCount")] - public long KeepCount { get; set; } - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; } -/// Autopilot objective file content, or null when missing. +/// Filesystem and environment context used to resolve model-picker settings. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspacesReadAutopilotObjectiveResult +public sealed class ModelPickerSettingsContext { - /// Autopilot objective file content, or null when missing. - [JsonPropertyName("content")] - public string? Content { get; set; } -} + /// Optional Copilot configuration directory containing persisted settings. + [JsonPropertyName("configDir")] + public string? ConfigDir { 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; -} + /// Environment variables consulted while resolving model-picker settings. + [JsonPropertyName("environment")] + public ModelPickerSettingsContextEnvironment Environment { get => field ??= new(); 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; + /// User home directory used when resolving persisted settings. + [JsonPropertyName("homeDirectory")] + public string HomeDirectory { get; set; } = string.Empty; } -/// Autopilot objective file content to persist. +/// RPC data type for ModelPickerPersistence operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class WorkspacesWriteAutopilotObjectiveRequest +public sealed class ModelPickerPersistenceRequest { - /// Autopilot objective file content. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// Whether context tier was explicitly selected and should be persisted. + [JsonPropertyName("contextTierExplicit")] + public bool? ContextTierExplicit { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Whether reasoning effort was explicitly selected and should be persisted. + [JsonPropertyName("reasoningEffortExplicit")] + public bool? ReasoningEffortExplicit { 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; } + /// Filesystem and environment context used to resolve settings persistence. + [JsonPropertyName("settingsContext")] + public ModelPickerSettingsContext SettingsContext { get => field ??= new(); set; } } -/// Identifies the target session. +/// Target model identifier and optional reasoning effort, summary, capability overrides, and context tier. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionWorkspacesDeleteAutopilotObjectiveRequest +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; } + + /// 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; } + + /// 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; } + + /// 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; } + + /// Override individual model capabilities resolved by the runtime. + [JsonPropertyName("modelCapabilities")] + public ModelCapabilitiesOverride? ModelCapabilities { get; set; } + + /// Settings scope used when persisting the selected model. + [JsonPropertyName("modelChangeScope")] + public string? ModelChangeScope { get; 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; + + /// Optional settings context and explicit-override flags used to persist a picker selection. + [JsonPropertyName("pickerPersistence")] + public ModelPickerPersistenceRequest? PickerPersistence { 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; } + + /// 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; -} -/// 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; } + /// 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; } + + /// Output verbosity level to request for supported models. + [JsonPropertyName("verbosity")] + public Verbosity? Verbosity { get; set; } } -/// Identifies the target session. +/// 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)] -internal sealed class SessionWorkspacesAutopilotObjectiveExistsRequest +public sealed class ModelSwitchAutoTierResult { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// 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; } -/// 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; + /// Auto preference currently committed for the session. + [JsonPropertyName("effectiveAutoTier")] + public AutoTier? EffectiveAutoTier { get; set; } - /// Absolute filesystem path to the saved paste file. - [JsonPropertyName("filePath")] - public string FilePath { get; set; } = string.Empty; + /// Latest unclaimed Auto preference waiting for a future user turn. + [JsonPropertyName("pendingAutoTier")] + public AutoTier? PendingAutoTier { get; set; } - /// Size of the saved file in bytes. - [JsonPropertyName("sizeBytes")] - public long SizeBytes { get; set; } -} + /// Immediate request status. `pending` means accepted but not committed. + [JsonPropertyName("status")] + public ModelSwitchAutoTierStatus Status { 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; } + /// 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; } } -/// Pasted content to save as a UTF-8 file in the session workspace. +/// 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 WorkspacesSaveLargePasteRequest +internal sealed class ModelSwitchAutoTierRequest { - /// Pasted content to save as a UTF-8 file. - [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; + + /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. + [JsonPropertyName("source")] + public ModelChangeSource? Source { get; set; } } -/// A single changed file and its unified diff. +/// Managed, repository, and CLI model overrides to overlay onto the session at startup. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkspaceDiffFileChange +internal sealed class ModelApplyStartupOverlayRequest { - /// 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; + /// 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; } - /// Whether the diff content was omitted because it exceeded the per-file size limit. - [JsonPropertyName("isTruncated")] - public bool? IsTruncated { get; set; } + /// Model explicitly selected by the CLI, when provided. + [JsonPropertyName("cliModel")] + public string? CliModel { get; set; } - /// Original file path for renamed files. - [JsonPropertyName("oldPath")] - public string? OldPath { get; set; } + /// Whether the overlay is being applied while resuming a deferred session. + [JsonPropertyName("deferredResume")] + public bool? DeferredResume { 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; -} + /// Model required by device-managed policy, when configured. + [JsonPropertyName("deviceManagedModel")] + public string? DeviceManagedModel { 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; } + /// 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; } - /// Changed files and their unified diffs. - [JsonPropertyName("changes")] - public IList Changes { get => field ??= []; 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; } - /// 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; } + /// 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; } - /// Effective mode used for the returned changes. - [JsonPropertyName("mode")] - public WorkspaceDiffMode Mode { 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; } - /// Diff mode requested by the client. - [JsonPropertyName("requestedMode")] - public WorkspaceDiffMode RequestedMode { get; set; } + /// Context tier selected by repository settings, when configured. + [JsonPropertyName("repoContextTier")] + public string? RepoContextTier { 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; } -} + /// Model selected by repository settings, when configured. + [JsonPropertyName("repoModel")] + public string? RepoModel { 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; } + /// Reasoning effort selected by repository settings, when configured. + [JsonPropertyName("repoReasoningEffort")] + public string? RepoReasoningEffort { get; set; } - /// Diff mode requested by the client. - [JsonPropertyName("mode")] - public WorkspaceDiffMode Mode { 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; } -/// Current per-window credit limit and consumption for an autopilot objective. +/// The applied host allowlist and effective session model policy after intersection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AutopilotObjectiveCreditLimit +public sealed class ModelSetAllowedModelsResult { - /// Configured AI-credit cap, when one is set. - [JsonPropertyName("credits")] - public double? Credits { 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; } - /// Window consumption in fractional AI credits, for display. - [JsonPropertyName("creditsUsed")] - public double CreditsUsed { 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; } - /// 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; + /// 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; } } -/// Public, persistence-independent projection of an autopilot objective. +/// 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)] -public sealed class AutopilotObjectiveState +internal sealed class ModelSetAllowedModelsRequest { - /// Optional summary recorded when the objective completed. - [JsonPropertyName("completionSummary")] - public string? CompletionSummary { 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; - - /// Current per-window consumption and optional cap, when a credit-tracking window is present. - [JsonPropertyName("creditLimit")] - public AutopilotObjectiveCreditLimit? CreditLimit { get; set; } - - /// Session-local objective identifier. - [JsonPropertyName("id")] - public long Id { get; set; } - - /// User-provided objective text. - [JsonPropertyName("objective")] - public string Objective { get; set; } = string.Empty; - - /// Optional reason the objective is paused. - [JsonPropertyName("pauseReason")] - public string? PauseReason { get; set; } - - /// Current normalized lifecycle status. - [JsonPropertyName("status")] - public AutopilotObjectiveStatus Status { get; set; } + /// Exact model IDs to permit, or null to clear the host restriction. + [JsonPropertyName("allowedModels")] + public IList? AllowedModels { get; set; } - /// Number of objective turns started. - [JsonPropertyName("turnCount")] - public long TurnCount { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Canonical runtime state for the session's current autopilot objective. +/// 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 AutopilotObjectiveGetStateResult +public sealed class ModelSetReasoningEffortResult { - /// Current objective state, or `null` when the session has no objective. - [JsonPropertyName("state")] - public AutopilotObjectiveState? State { get; set; } + /// Reasoning effort level recorded on the session after the update. + [JsonPropertyName("reasoningEffort")] + public string ReasoningEffort { get; set; } = string.Empty; } -/// Identifies the target session. +/// Reasoning effort level to apply to the currently selected model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAutopilotObjectiveGetStateRequest +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; + /// 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`). +/// Cost-category metadata for a CAPI model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CompletionsGetTriggerCharactersResult +public sealed class SessionModelPriceCategory { - /// Trigger characters advertised by the host (e.g. `["@", "#"]`). Empty disables host-driven completions for the session. - [JsonPropertyName("triggerCharacters")] - public IList TriggerCharacters { get => field ??= []; 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 SessionCompletionsGetTriggerCharactersRequest -{ - /// 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; } } -/// 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. +/// 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 SessionCompletionItem +public sealed class ModelProviderDescriptor { - /// Text spliced into the composer when the item is accepted. - [JsonPropertyName("insertText")] - public string InsertText { get; set; } = string.Empty; + /// Opaque, stable provider id, stamped onto every model this provider returns. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// Render-kind hint for the picker row (e.g. `"document"`, `"directory"`), derived from the host's display kind. + /// The neutral provider kind. [JsonPropertyName("kind")] - public string? Kind { get; set; } + public ModelProviderKind Kind { get; set; } - /// Primary display label for the picker row. Falls back to `insertText` when absent. + /// Human-readable menu label, owned by the runtime so every consumer renders identical text. [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; } + public string Label { get; set; } = string.Empty; - /// Start of the replacement range in `text`, in UTF-16 code units. - [JsonPropertyName("rangeStart")] - public long? RangeStart { get; set; } + /// Stable ordering key for presenting providers in a deterministic sequence. + [JsonPropertyName("ordering")] + public long Ordering { get; set; } } -/// Host-driven completion items for the current composer input. Empty when the host returns no items or does not support completions. +/// The list of models available to this session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CompletionsRequestResult +public sealed class SessionModelList { - /// Completion items in host-ranked order. - [JsonPropertyName("items")] - public IList Items { get => field ??= []; set; } -} + /// Available models, ordered with the most preferred default first. Includes both Copilot (CAPI) models and any registry BYOK models; a BYOK model appears under its provider-qualified selection id (`provider/id`). + [JsonPropertyName("list")] + public IList List { get => field ??= []; set; } -/// Request host-driven completions for the current composer input. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CompletionsRequestRequest -{ - /// Cursor offset within `text`, in UTF-16 code units. - [JsonPropertyName("offset")] - public long Offset { get; 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; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { 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; } - /// The full composed composer input. - [JsonPropertyName("text")] - public string Text { get; set; } = string.Empty; + /// Per-quota snapshots returned alongside the model list, keyed by quota type. + [JsonPropertyName("quotaSnapshots")] + public IDictionary? QuotaSnapshots { get; set; } } -/// Instruction sources loaded for the session, in merge order. +/// RPC data type for SessionModelList operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class InstructionsGetSourcesResult +public sealed class SessionModelListRequest { - /// Instruction sources for the session. - [JsonPropertyName("sources")] - public IList Sources { get => field ??= []; set; } + /// If true, bypasses the per-session model list cache and re-fetches from CAPI. + [JsonPropertyName("skipCache")] + public bool? SkipCache { get; set; } } -/// Identifies the target session. +/// RPC data type for SessionModelListRequestWithSession operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionInstructionsGetSourcesRequest +internal sealed class SessionModelListRequestWithSession { /// 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; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionInstructionsReloadRequest +internal sealed class SessionModeGetRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Diagnostics from reloading skill definitions, with warnings and errors as separate lists. +/// 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 SkillsLoadDiagnostics +public sealed class ModeSetResult { - /// Errors emitted while loading skills (e.g. skills that failed to load entirely). - [JsonPropertyName("errors")] - public IList Errors { get => field ??= []; set; } + /// Whether the host should arm an interactive continuation after the mode change. + [JsonPropertyName("armInteractiveContinuation")] + public bool? ArmInteractiveContinuation { get; set; } - /// Warnings emitted while loading skills (e.g. skills that loaded but had issues). - [JsonPropertyName("warnings")] - public IList Warnings { get => field ??= []; set; } -} + /// Compaction confirmation required before the mode change can complete. + [JsonPropertyName("confirmation")] + public ModelSwitchConfirmation? Confirmation { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionCustomizationsReloadRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Whether the host must defer implementing the requested mode change. + [JsonPropertyName("deferImplementation")] + public bool? DeferImplementation { get; set; } -/// 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; } -} + /// Deprecation warnings associated with the model selected by the mode change. + [JsonPropertyName("deprecationWarnings")] + public IList? DeprecationWarnings { 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. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FleetStartRequest -{ - /// Optional attachments (files, directories, selections, blobs, GitHub references) to include with the fleet request. - [JsonPropertyName("attachments")] - public IList? Attachments { get; set; } + /// User-facing outcome message for the model switch triggered by the mode change. + [JsonPropertyName("message")] + public string? Message { 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; } + /// 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; } - /// Optional user prompt to combine with fleet instructions. - [JsonPropertyName("prompt")] - public string? Prompt { get; set; } + /// Whether applying the mode changed the active model. + [JsonPropertyName("modelChanged")] + public bool ModelChanged { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Lifecycle status of the requested mode change. + [JsonPropertyName("status")] + public string Status { 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; } + /// User-facing warning produced while applying the mode change. + [JsonPropertyName("warning")] + public string? Warning { get; set; } } -/// Agents available to the session. +/// Agent interaction mode to apply to the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AgentList +internal sealed class ModeSetRequest { - /// Available agents. - [JsonPropertyName("agents")] - public IList Agents { get => field ??= []; set; } -} + /// Explicit response to a model-switch compaction preflight. + [JsonPropertyName("compactionDecision")] + public string? CompactionDecision { get; set; } -/// RPC data type for SessionAgentList operations. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionAgentListRequest -{ - /// 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; } + /// 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; } - /// 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; } -} + /// Session whose plan-mode base state should be inherited. + [JsonPropertyName("inheritPlanBaseFromSessionId")] + public string? InheritPlanBaseFromSessionId { get; set; } -/// RPC data type for SessionAgentListRequestWithSession operations. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAgentListRequestWithSession -{ - /// 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; } + /// The session mode the agent is operating in. + [JsonPropertyName("mode")] + public SessionMode Mode { 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; } + /// Whether the selected plan model should be persisted. + [JsonPropertyName("persistPlanSelection")] + public bool? PersistPlanSelection { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Settings context used when persisting the selected plan model. + [JsonPropertyName("pickerSettingsContext")] + public ModelPickerSettingsContext? PickerSettingsContext { get; set; } -/// An in-memory authored prompt override for an available agent. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AgentSetPromptRequest -{ - /// Stable effective agent id. Plugin namespace separators are normalized. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Context tier to use with the dedicated plan model. + [JsonPropertyName("planContextTier")] + public string? PlanContextTier { get; set; } - /// Replacement authored prompt. Empty text is valid. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Action to perform when leaving plan mode. + [JsonPropertyName("planExitAction")] + public string? PlanExitAction { get; set; } + + /// Dedicated model to use in plan mode, when configured. + [JsonPropertyName("planModel")] + public string? PlanModel { get; set; } + + /// Whether a dedicated plan model is configured. + [JsonPropertyName("planModelConfigured")] + public bool? PlanModelConfigured { get; set; } + + /// Reasoning effort to use with the dedicated plan model. + [JsonPropertyName("planReasoningEffort")] + public string? PlanReasoningEffort { get; set; } + + /// Whether leaving plan mode should restore the session's previous model. + [JsonPropertyName("restorePlanModel")] + public bool? RestorePlanModel { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// The currently selected custom agent, or null when using the default agent. +/// The session's friendly name, or null when not yet set. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AgentGetCurrentResult +public sealed class NameGetResult { - /// Currently selected custom agent, or null if using the default agent. - [JsonPropertyName("agent")] - public AgentInfo? Agent { get; 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 SessionAgentGetCurrentRequest +internal sealed class SessionNameGetRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// The newly selected custom agent. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AgentSelectResult -{ - /// The newly selected custom agent. - [JsonPropertyName("agent")] - public AgentInfo Agent { get => field ??= new(); set; } -} - -/// Name of the custom agent to select for subsequent turns. +/// New friendly name to apply to the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class AgentSelectRequest +internal sealed class NameSetRequest { - /// Name of the custom agent to select. + /// 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; @@ -12889,1126 +13337,880 @@ internal sealed class AgentSelectRequest public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// Indicates whether the auto-generated summary was applied as the session's name. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionAgentDeselectRequest +public sealed class NameSetAutoResult +{ + /// 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; } +} + +/// 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; } -/// Custom agents available to the session after reloading definitions from disk. +/// Existence, contents, and resolved path of the session plan file. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class AgentReloadResult +public sealed class PlanReadResult { - /// Reloaded custom agents. - [JsonPropertyName("agents")] - public IList Agents { 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 SessionAgentReloadRequest +internal sealed class SessionPlanReadRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Identifier assigned to the newly started background agent task. +/// Replacement contents to write to the session plan file. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksStartAgentResult +internal sealed class PlanUpdateRequest { - /// Generated agent ID for the background task. - [JsonPropertyName("agentId")] - public string AgentId { get; set; } = string.Empty; + /// 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; } -/// Agent type, prompt, name, and optional description and model override for the new task. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksStartAgentRequest +internal sealed class SessionPlanDeleteRequest { - /// Type of agent to start (e.g., 'explore', 'task', 'general-purpose'). - [JsonPropertyName("agentType")] - public string AgentType { get; set; } = string.Empty; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Short description of the task. +/// 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)] +public sealed class PlanSqlTodosRow +{ + /// 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; } - /// Optional model override. - [JsonPropertyName("model")] - public string? Model { get; set; } + /// Todo identifier. + [JsonPropertyName("id")] + public string? Id { get; set; } - /// Friendly, non-unique name used when displaying the agent. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Todo status. + [JsonPropertyName("status")] + public string? Status { get; set; } - /// Task prompt for the agent. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Todo title. + [JsonPropertyName("title")] + public string? Title { get; set; } +} + +/// Todo rows read from the session SQL database. Empty when no session database is available. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PlanReadSqlTodosResult +{ + /// 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; } +} +/// 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; } -/// Tracked task union returned by task APIs, containing an agent, client, or shell task. -/// Polymorphic base type discriminated by type. +/// 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)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "type", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(TaskInfoAgent), "agent")] -[JsonDerivedType(typeof(TaskInfoClient), "client")] -[JsonDerivedType(typeof(TaskInfoShell), "shell")] -public partial class TaskInfo +public sealed class PlanSqlTodoDependency { - /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; -} + /// 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; +} -/// Tracked background agent task metadata, including IDs, status, timing, agent type, prompt, model, result, and latest response. -/// The agent variant of . +/// Todo rows + dependency edges read from the session SQL database. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskInfoAgent : TaskInfo +public sealed class PlanReadSqlTodosWithDependenciesResult { - /// - [JsonIgnore] - public override string Type => "agent"; + /// 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; } - /// ISO 8601 timestamp when the current active period began. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("activeStartedAt")] - public DateTimeOffset? ActiveStartedAt { 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; } +} - /// Accumulated active execution time in milliseconds. - [JsonConverter(typeof(MillisecondsTimeSpanConverter))] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("activeTimeMs")] - public TimeSpan? ActiveTime { 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; +} - /// Type of agent running this task. - [JsonPropertyName("agentType")] - public required string AgentType { get; set; } +/// RPC data type for WorkspacesGetWorkspaceResultWorkspace operations. +public sealed class WorkspacesGetWorkspaceResultWorkspace +{ + /// Current Git branch. + [JsonPropertyName("branch")] + public string? Branch { 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; } + /// Whether the per-session Chronicle upgrade prompt was dismissed for the workspace. + [JsonPropertyName("chronicle_sync_dismissed")] + public bool? ChronicleSyncDismissed { get; set; } - /// ISO 8601 timestamp when the task finished. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("completedAt")] - public DateTimeOffset? CompletedAt { get; set; } + /// Name of the client that created the workspace. + [JsonPropertyName("client_name")] + public string? ClientName { get; set; } - /// Short description of the task. - [JsonPropertyName("description")] - public required string Description { get; set; } + /// Timestamp when the workspace was created. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } - /// Friendly, non-unique name intended for display. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } + /// Current working directory associated with the workspace. + [JsonPropertyName("cwd")] + public string? Cwd { get; set; } - /// Error message when the task failed. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Git repository root associated with the workspace. + [JsonPropertyName("git_root")] + public string? GitRoot { get; set; } - /// Whether task execution is synchronously awaited or managed in the background. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("executionMode")] - public TaskExecutionMode? ExecutionMode { get; set; } + /// Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. + [JsonPropertyName("host_type")] + public WorkspacesWorkspaceDetailsHostType? HostType { get; set; } - /// Unique task identifier. + /// 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 required string Id { get; set; } + public string Id { get; set; } = string.Empty; - /// ISO 8601 timestamp when the agent entered idle state. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("idleSince")] - public DateTimeOffset? IdleSince { get; set; } + /// Most recent Mission Control event identifier observed for the workspace. + [JsonPropertyName("mc_last_event_id")] + public string? McLastEventId { get; set; } - /// Most recent response text from the agent. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("latestResponse")] - public string? LatestResponse { get; set; } + /// Mission Control session identifier associated with the workspace. + [JsonPropertyName("mc_session_id")] + public string? McSessionId { get; set; } - /// Requested model override for the task when specified. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("model")] - public string? Model { get; set; } + /// Mission Control task identifier associated with the workspace. + [JsonPropertyName("mc_task_id")] + public string? McTaskId { get; set; } - /// Most recent prompt delivered to the agent. Updated whenever the agent receives a follow-up message. - [JsonPropertyName("prompt")] - public required string Prompt { get; set; } + /// Workspace display name. + [JsonPropertyName("name")] + public string? Name { get; set; } - /// Runtime model resolved for the task when available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("resolvedModel")] - public string? ResolvedModel { get; set; } + /// Whether the workspace session can be steered remotely. + [JsonPropertyName("remote_steerable")] + public bool? RemoteSteerable { get; set; } - /// Result text from the task when available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("result")] - public string? Result { get; set; } + /// Repository identifier associated with the workspace. + [JsonPropertyName("repository")] + public string? Repository { get; set; } - /// ISO 8601 timestamp when the task was started. - [JsonPropertyName("startedAt")] - public required DateTimeOffset StartedAt { get; set; } + /// Number of persisted summaries in the workspace. + [JsonPropertyName("summary_count")] + public long? SummaryCount { get; set; } - /// Current lifecycle status of the task. - [JsonPropertyName("status")] - public required TaskStatus Status { get; set; } + /// Timestamp when the workspace was last updated. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } - /// Tool call ID associated with this agent task. - [JsonPropertyName("toolCallId")] - public required string ToolCallId { get; set; } + /// Whether the workspace name was explicitly chosen by the user. + [JsonPropertyName("user_named")] + public bool? UserNamed { get; set; } } -/// Public owner attribution for a client-owned task. Identifiers are opaque and never authorize requests. +/// Current workspace metadata for the session, including its absolute filesystem path when available. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TaskClientOwner +public sealed class WorkspacesGetWorkspaceResult { - /// ISO 8601 timestamp when the bound join disconnected. - [JsonPropertyName("disconnectedAt")] - public DateTimeOffset? DisconnectedAt { get; set; } - - /// Display-only owner name. - [JsonPropertyName("displayName")] - public string? DisplayName { 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; } - /// Opaque identity of the currently or most recently bound session join. - [JsonPropertyName("joinId")] - public string JoinId { get; set; } = string.Empty; + /// Current workspace metadata, or null if not available. + [JsonPropertyName("workspace")] + public WorkspacesGetWorkspaceResultWorkspace? Workspace { get; set; } +} - /// Class of the task owner. - [JsonPropertyName("kind")] - public TaskClientOwnerKind Kind { get; set; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionWorkspacesGetWorkspaceRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Opaque session-scoped participant identity. - [JsonPropertyName("participantId")] - public string ParticipantId { 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; } - /// Whether this task's bound join is currently connected. - [JsonPropertyName("presence")] - public TaskClientOwnerPresence Presence { get; set; } + /// Optional workspace display name override. + [JsonPropertyName("name")] + public string? Name { get; set; } - /// Display-only owner source. - [JsonPropertyName("source")] - public string? Source { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Tracked client-owned task metadata. -/// The client variant of . +/// Optional session context used when creating a local workspace. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskInfoClient : TaskInfo +internal sealed class WorkspacesEnsureRequest { - /// - [JsonIgnore] - public override string Type => "client"; - - /// ISO 8601 timestamp when the current active segment started. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("activeStartedAt")] - public DateTimeOffset? ActiveStartedAt { get; set; } + /// Opaque workspace context supplied by the session host. + [JsonPropertyName("context")] + public JsonElement? Context { get; set; } - /// Accumulated active execution time in milliseconds. - [JsonPropertyName("activeTimeMs")] - public required long ActiveTimeMs { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Whether the currently bound owner can receive a cancellation request. - [JsonPropertyName("canCancel")] - public required bool CanCancel { get; set; } +/// Relative paths of files stored in the session workspace files directory. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class WorkspacesListFilesResult +{ + /// Slash-separated relative file paths in the workspace files directory. + [JsonPropertyName("files")] + public IList Files { get => field ??= []; set; } +} - /// Human-readable reason for terminal cancellation. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("cancellationReason")] - public string? CancellationReason { get; set; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionWorkspacesListFilesRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Owner-scoped registration and reclaim key. - [JsonPropertyName("clientTaskId")] - public required string ClientTaskId { get; set; } - - /// ISO 8601 timestamp when the task reached a terminal status. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("completedAt")] - public DateTimeOffset? CompletedAt { get; set; } - - /// Task description. - [JsonPropertyName("description")] - public required string Description { get; set; } - - /// Optional task display name. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } - - /// Human-readable terminal failure message. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("error")] - public string? Error { get; set; } - - /// Optional owner-supplied terminal failure code. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("errorCode")] - public string? ErrorCode { get; set; } - - /// Execution mode, which is always background for client-owned tasks. - [JsonPropertyName("executionMode")] - public required TaskClientExecutionMode ExecutionMode { get; set; } - - /// Canonical runtime-generated task identifier. - [JsonPropertyName("id")] - public required string Id { get; set; } - - /// ISO 8601 timestamp when the connected owner entered idle status. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("idleSince")] - public DateTimeOffset? IdleSince { get; set; } - - /// ISO 8601 timestamp of the most recent orphan transition. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("orphanedAt")] - public DateTimeOffset? OrphanedAt { get; set; } - - /// Public attribution and presence for the task owner. - [JsonPropertyName("owner")] - public required TaskClientOwner Owner { get; set; } - - /// ISO 8601 timestamp of the most recent successful reclaim. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("reclaimedAt")] - public DateTimeOffset? ReclaimedAt { get; set; } - - /// Opaque successful terminal result supplied by the task owner. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } - - /// Sequence number of the latest accepted owner update. - [JsonPropertyName("sequence")] - public required long Sequence { get; set; } - - /// ISO 8601 timestamp when the task started. - [JsonPropertyName("startedAt")] - public required DateTimeOffset StartedAt { get; set; } - - /// 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; } -} - -/// Tracked shell task metadata, including ID, command, status, timing, attachment/execution mode, log path, and PID. -/// The shell variant of . +/// Contents of the requested workspace file as a UTF-8 string. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskInfoShell : TaskInfo +public sealed class WorkspacesReadFileResult { - /// - [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; } - - /// Whether this shell task can be promoted to background mode. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("canPromoteToBackground")] - public bool? CanPromoteToBackground { get; set; } - - /// Command being executed. - [JsonPropertyName("command")] - public required string Command { get; set; } - - /// ISO 8601 timestamp when the task finished. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("completedAt")] - public DateTimeOffset? CompletedAt { get; set; } - - /// Short description of the task. - [JsonPropertyName("description")] - public required string Description { get; set; } - - /// Whether task execution is synchronously awaited or managed in the background. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("executionMode")] - public TaskExecutionMode? ExecutionMode { get; set; } - - /// Unique task identifier. - [JsonPropertyName("id")] - public required string Id { get; set; } - - /// Path to the detached shell log, when available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("logPath")] - public string? LogPath { get; set; } - - /// Process ID when available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("pid")] - public long? Pid { 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; } + /// File content as a UTF-8 string. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; } -/// Background tasks currently tracked by the session. +/// Relative path of the workspace file to read. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TaskList +internal sealed class WorkspacesReadFileRequest { - /// Currently tracked tasks. - [JsonPropertyName("tasks")] - public IList Tasks { get => field ??= []; set; } -} + /// Slash-separated relative path within the workspace files directory. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; -/// 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. +/// Relative path and UTF-8 content for the workspace file to create or overwrite. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TaskClientInfo +internal sealed class WorkspacesCreateFileRequest { - /// 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; } - - /// 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; } - - /// Optional owner-supplied terminal failure code. - [JsonPropertyName("errorCode")] - public string? ErrorCode { get; set; } - - /// Execution mode, which is always background for client-owned tasks. - [JsonPropertyName("executionMode")] - public TaskClientExecutionMode ExecutionMode { get; set; } - - /// Canonical runtime-generated task 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; } - - /// 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; } + /// File content to write as a UTF-8 string. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; - /// Task kind. - [JsonPropertyName("type")] - public TaskClientType Type { get; set; } + /// Slash-separated relative path within the workspace files directory. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// ISO 8601 timestamp of the latest accepted lifecycle change. - [JsonPropertyName("updatedAt")] - public DateTimeOffset UpdatedAt { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Result of registering or reclaiming a client-owned task. +/// Filesystem metadata for a path in the session workspace files directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksRegisterResult +public sealed class WorkspacesStatFileResult { - /// True only when this invocation created a new task. - [JsonPropertyName("created")] - public bool Created { get; set; } + /// Creation time in Unix epoch milliseconds. + [JsonPropertyName("birthtimeMs")] + public double BirthtimeMs { get; set; } - /// True only when this invocation reclaimed an orphaned task. - [JsonPropertyName("reclaimed")] - public bool Reclaimed { get; set; } + /// Whether the path identifies a directory. + [JsonPropertyName("isDirectory")] + public bool IsDirectory { get; set; } - /// Authoritative registered or reclaimed task. - [JsonPropertyName("task")] - public TaskClientInfo Task { get => field ??= new(); set; } + /// Whether the path identifies a regular file. + [JsonPropertyName("isFile")] + public bool IsFile { get; set; } + + /// Last modification time in Unix epoch milliseconds. + [JsonPropertyName("mtimeMs")] + public double MtimeMs { get; set; } + + /// Size in bytes. + [JsonPropertyName("size")] + public double Size { get; set; } } -/// Registers or reclaims a client-owned task. +/// Relative path of the workspace file or directory to inspect. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksRegisterRequest +internal sealed class WorkspacesStatFileRequest { - /// 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; } + /// 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; - - /// Task kind. - [JsonPropertyName("type")] - public TaskClientType Type { get; set; } } -/// Result of publishing a client-owned task update. +/// Directory to create within the session workspace files directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksUpdateResult +internal sealed class WorkspacesCreateDirectoryRequest { - /// 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; } + /// Slash-separated relative path within the workspace files directory. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Authoritative task after processing the update. - [JsonPropertyName("task")] - public TaskClientInfo Task { get => field ??= new(); set; } -} + /// Whether to create missing parent directories. Defaults to false. + [JsonPropertyName("recursive")] + public bool? Recursive { get; set; } -/// Progress or terminal update for a client-owned task. -/// Polymorphic base type discriminated by kind. -[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 -{ - /// 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 . +/// File or directory to remove from the session workspace files directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskClientUpdateProgress : TaskClientUpdate +internal sealed class WorkspacesRemovePathRequest { - /// - [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; } + /// Whether a missing path should be treated as success. Defaults to false. + [JsonPropertyName("force")] + public bool? Force { get; set; } - /// Optional completion percentage; null clears the current percentage. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("percentage")] - public double? Percentage { get; set; } + /// Slash-separated relative path within the workspace files directory. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Optional progress phase; null clears the current phase. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("phase")] - public string? Phase { get; set; } + /// Whether to remove directory contents recursively. Defaults to false. + [JsonPropertyName("recursive")] + public bool? Recursive { get; set; } - /// Optional active status transition. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("status")] - public TaskClientActiveStatus? Status { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Reports successful terminal completion. -/// The completed variant of . +/// Source and destination paths for a rename within the session workspace files directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskClientUpdateCompleted : TaskClientUpdate +internal sealed class WorkspacesRenamePathRequest { - /// - [JsonIgnore] - public override string Kind => "completed"; + /// Slash-separated destination path relative to the workspace files directory. + [JsonPropertyName("destination")] + public string Destination { get; set; } = string.Empty; - /// Optional final progress message. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("message")] - public string? Message { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Optional opaque successful terminal result. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("result")] - public JsonElement? Result { get; set; } + /// Slash-separated source path relative to the workspace files directory. + [JsonPropertyName("source")] + public string Source { get; set; } = string.Empty; } -/// Reports terminal failure. -/// The failed variant of . +/// Workspace checkpoint metadata with assigned number, human-readable title, and checkpoint filename. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskClientUpdateFailed : TaskClientUpdate +public sealed class WorkspacesCheckpoints { - /// - [JsonIgnore] - public override string Kind => "failed"; - - /// Optional owner-supplied terminal failure code. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("code")] - public string? Code { get; set; } + /// Filename of the checkpoint within the workspace checkpoints directory. + [JsonPropertyName("filename")] + public string Filename { get; set; } = string.Empty; - /// Human-readable terminal failure message. - [JsonPropertyName("error")] - public required string Error { get; set; } + /// Checkpoint number assigned by the workspace manager. + [JsonPropertyName("number")] + public long Number { get; set; } - /// Optional final progress message. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("message")] - public string? Message { get; set; } + /// Human-readable checkpoint title. + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; } -/// Reports terminal cancellation after external work stopped. -/// The cancelled variant of . +/// Workspace checkpoints in chronological order; empty when the workspace is not enabled. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskClientUpdateCancelled : TaskClientUpdate +public sealed class WorkspacesListCheckpointsResult { - /// - [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; } + /// Workspace checkpoints in chronological order. Empty when workspace is not enabled. + [JsonPropertyName("checkpoints")] + public IList Checkpoints { get => field ??= []; set; } } -/// Updates a client-owned task. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksUpdateRequest +internal sealed class SessionWorkspacesListCheckpointsRequest { - /// 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; } } -/// 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. +/// 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 TasksRefreshResult +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; } } -/// Identifies the target session. +/// Checkpoint number to read. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionTasksRefreshRequest +internal sealed class WorkspacesReadCheckpointRequest { + /// Checkpoint number to read. + [JsonPropertyName("number")] + public long Number { get; set; } + /// 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 +/// Metadata for the persisted summary. +public sealed class WorkspacesAddSummaryResultSummary { } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionTasksWaitForPendingRequest +/// Refreshed metadata for the containing workspace. +public sealed class WorkspacesAddSummaryResultWorkspace { - /// 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. +/// Persisted summary metadata and refreshed workspace metadata. [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 -{ - /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; -} - - -/// 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; - - /// ISO 8601 timestamp when this event occurred. - [JsonPropertyName("timestamp")] - public DateTimeOffset Timestamp { get; set; } -} - -/// 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 partial class TaskProgressAgent : TaskProgress -{ - /// - [JsonIgnore] - public override string Type => "agent"; - - /// The most recent intent reported by the agent. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("latestIntent")] - public string? LatestIntent { get; set; } - - /// Recent tool execution events converted to display lines. - [JsonPropertyName("recentActivity")] - public required IList RecentActivity { get; set; } -} - -/// Generic progress for a client-owned task. -/// The client variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskProgressClient : TaskProgress +public sealed class WorkspacesAddSummaryResult { - /// - [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; } + /// Metadata for the persisted summary. + [JsonPropertyName("summary")] + public WorkspacesAddSummaryResultSummary? Summary { get; set; } - /// ISO 8601 timestamp of the latest accepted lifecycle change. - [JsonPropertyName("updatedAt")] - public required DateTimeOffset UpdatedAt { get; set; } + /// Refreshed metadata for the containing workspace. + [JsonPropertyName("workspace")] + public WorkspacesAddSummaryResultWorkspace? Workspace { get; set; } } -/// Progress snapshot for a shell task, with recent stdout/stderr output and optional process ID. -/// The shell variant of . +/// Compaction summary checkpoint to persist. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class TaskProgressShell : TaskProgress +internal sealed class WorkspacesAddSummaryRequest { - /// - [JsonIgnore] - public override string Type => "shell"; - - /// Process ID when available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("pid")] - public long? Pid { get; set; } + /// Markdown summary content to persist. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; - /// Recent stdout/stderr lines from the running shell command. - [JsonPropertyName("recentOutput")] - public required string RecentOutput { get; set; } -} + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; -/// Progress information for the task, or null when no task with that ID is tracked. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksGetProgressResult -{ - /// 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; } + /// Summary title shown in checkpoint listings. + [JsonPropertyName("title")] + public string Title { get; set; } = string.Empty; } -/// Identifier of the background task to fetch progress for. +/// Rollback point for local workspace summaries. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksGetProgressRequest +internal sealed class WorkspacesTruncateSummariesRequest { - /// Task identifier (agent ID or shell ID). - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Number of newest summaries to keep. + [JsonPropertyName("keepCount")] + public long KeepCount { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// The first sync-waiting task that can currently be promoted to background mode. +/// Autopilot objective file content, or null when missing. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksGetCurrentPromotableResult +public sealed class WorkspacesReadAutopilotObjectiveResult { - /// 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; } + /// Autopilot objective file content, or null when missing. + [JsonPropertyName("content")] + public string? Content { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionTasksGetCurrentPromotableRequest +internal sealed class SessionWorkspacesReadAutopilotObjectiveRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the task was successfully promoted to background mode. +/// Result of writing the autopilot objective file. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksPromoteToBackgroundResult +public sealed class WorkspacesWriteAutopilotObjectiveResult { - /// Whether the task was successfully promoted to background mode. - [JsonPropertyName("promoted")] - public bool Promoted { get; set; } + /// Filesystem operation performed. + [JsonPropertyName("operation")] + public string Operation { get; set; } = string.Empty; } -/// Identifier of the task to promote to background mode. +/// Autopilot objective file content to persist. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksPromoteToBackgroundRequest +internal sealed class WorkspacesWriteAutopilotObjectiveRequest { - /// Task identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Autopilot objective file content. + [JsonPropertyName("content")] + public string Content { 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. +/// Result of deleting the autopilot objective file. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksPromoteCurrentToBackgroundResult +public sealed class WorkspacesDeleteAutopilotObjectiveResult { - /// 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; } + /// True when a file was deleted. + [JsonPropertyName("deleted")] + public bool Deleted { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionTasksPromoteCurrentToBackgroundRequest +internal sealed class SessionWorkspacesDeleteAutopilotObjectiveRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the background task was successfully cancelled. +/// Whether the autopilot objective file exists. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksCancelResult +public sealed class WorkspacesAutopilotObjectiveExistsResult { - /// Whether the task was successfully cancelled. - [JsonPropertyName("cancelled")] - public bool Cancelled { get; set; } + /// True when the objective file exists. + [JsonPropertyName("exists")] + public bool Exists { get; set; } } -/// Identifier of the background task to cancel. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksCancelRequest +internal sealed class SessionWorkspacesAutopilotObjectiveExistsRequest { - /// 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 task was removed. False when the task does not exist or is still running/idle. +/// 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; + + /// Absolute filesystem path to the saved paste file. + [JsonPropertyName("filePath")] + public string FilePath { get; set; } = string.Empty; + + /// Size of the saved file in bytes. + [JsonPropertyName("sizeBytes")] + public long SizeBytes { get; set; } +} + +/// Descriptor for the saved paste file, or null when the workspace is unavailable. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksRemoveResult +public sealed class WorkspacesSaveLargePasteResult { - /// 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; } + /// 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; } } -/// Identifier of the completed or cancelled task to remove from tracking. +/// Pasted content to save as a UTF-8 file in the session workspace. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksRemoveRequest +internal sealed class WorkspacesSaveLargePasteRequest { - /// Task identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// 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; } -/// Indicates whether the message was delivered, with an error message when delivery failed. +/// A single changed file and its unified diff. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TasksSendMessageResult +public sealed class WorkspaceDiffFileChange { - /// Error message if delivery failed. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Type of change represented by this file diff. + [JsonPropertyName("changeType")] + public WorkspaceDiffFileChangeType ChangeType { get; set; } - /// Whether the message was successfully delivered or steered. - [JsonPropertyName("sent")] - public bool Sent { 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; } -/// Identifier of the target agent task, message content, and optional sender agent ID. +/// Workspace diff result for the requested mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TasksSendMessageRequest +public sealed class WorkspaceDiffResult { - /// Agent ID of the sender, if sent on behalf of another agent. - [JsonPropertyName("fromAgentId")] - public string? FromAgentId { get; set; } + /// Default branch used for a branch diff, when branch mode was requested. + [JsonPropertyName("baseBranch")] + public string? BaseBranch { get; set; } - /// Agent task identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Changed files and their unified diffs. + [JsonPropertyName("changes")] + public IList Changes { get => field ??= []; set; } - /// Message content to send to the agent. - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; + /// 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; } + + /// Effective mode used for the returned changes. + [JsonPropertyName("mode")] + public WorkspaceDiffMode Mode { get; set; } + + /// Diff mode requested by the client. + [JsonPropertyName("requestedMode")] + public WorkspaceDiffMode RequestedMode { 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; } +} + +/// 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; } + + /// Diff mode requested by the client. + [JsonPropertyName("mode")] + public WorkspaceDiffMode Mode { get; set; } /// 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. +/// Current per-window credit limit and consumption for an autopilot objective. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class Skill +public sealed class AutopilotObjectiveCreditLimit { - /// Optional freeform hint describing the skill's expected arguments, from the `argument-hint` frontmatter field. - [JsonPropertyName("argumentHint")] - public string? ArgumentHint { get; set; } + /// Configured AI-credit cap, when one is set. + [JsonPropertyName("credits")] + public double? Credits { get; set; } - /// Canonical slash command name used to invoke the skill, without the leading '/'. - [JsonPropertyName("commandName")] - public string? CommandName { get; set; } + /// Window consumption in fractional AI credits, for display. + [JsonPropertyName("creditsUsed")] + public double CreditsUsed { get; set; } - /// Description of what the skill does. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; + /// 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; +} - /// Whether the skill is currently enabled. - [JsonPropertyName("enabled")] - public bool Enabled { 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; } - /// Unique identifier for the skill. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// 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; - /// Absolute path to the skill file. - [JsonPropertyName("path")] - public string? Path { get; set; } + /// Current per-window consumption and optional cap, when a credit-tracking window is present. + [JsonPropertyName("creditLimit")] + public AutopilotObjectiveCreditLimit? CreditLimit { get; set; } - /// Name of the plugin that provides the skill, when source is 'plugin'. - [JsonPropertyName("pluginName")] - public string? PluginName { get; set; } + /// Session-local objective identifier. + [JsonPropertyName("id")] + public long Id { get; set; } - /// Source location type (e.g., project, personal-copilot, plugin, builtin). - [JsonPropertyName("source")] - public SkillSource Source { get; set; } + /// User-provided objective text. + [JsonPropertyName("objective")] + public string Objective { get; set; } = string.Empty; - /// Whether the skill can be invoked by the user as a slash command. - [JsonPropertyName("userInvocable")] - public bool UserInvocable { get; set; } + /// Optional reason the objective is paused. + [JsonPropertyName("pauseReason")] + public string? PauseReason { get; set; } + + /// Current normalized lifecycle status. + [JsonPropertyName("status")] + public AutopilotObjectiveStatus Status { get; set; } + + /// Number of objective turns started. + [JsonPropertyName("turnCount")] + public long TurnCount { get; set; } } -/// Skills available to the session, with their enabled state. +/// Canonical runtime state for the session's current autopilot objective. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SkillList +public sealed class AutopilotObjectiveGetStateResult { - /// Available skills. - [JsonPropertyName("skills")] - public IList Skills { get => field ??= []; set; } + /// Current objective state, or `null` when the session has no objective. + [JsonPropertyName("state")] + public AutopilotObjectiveState? State { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSkillsListRequest +internal sealed class SessionAutopilotObjectiveGetStateRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Skill invocation record with name, path, content, allowed tools, and turn number. +/// 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 SkillsInvokedSkill +public sealed class CompletionsGetTriggerCharactersResult { - /// Tools that should be auto-approved when this skill is active, captured at invocation time. - [JsonPropertyName("allowedTools")] - public IList? AllowedTools { get; 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; } +} - /// Full content of the skill file. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionCompletionsGetTriggerCharactersRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Whether model invocation was disabled when this skill was invoked. - [JsonPropertyName("disableModelInvocation")] - public bool? DisableModelInvocation { get; set; } +/// 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 sealed class SessionCompletionItem +{ + /// Text spliced into the composer when the item is accepted. + [JsonPropertyName("insertText")] + public string InsertText { get; set; } = string.Empty; - /// Turn number when the skill was invoked. - [JsonPropertyName("invokedAtTurn")] - public long InvokedAtTurn { get; set; } + /// 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; } - /// Unique identifier for the skill. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Primary display label for the picker row. Falls back to `insertText` when absent. + [JsonPropertyName("label")] + public string? Label { 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; -} + /// End (exclusive) of the replacement range in `text`, in UTF-16 code units. + [JsonPropertyName("rangeEnd")] + public long? RangeEnd { 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; } + /// Start of the replacement range in `text`, in UTF-16 code units. + [JsonPropertyName("rangeStart")] + public long? RangeStart { get; set; } } -/// Identifies the target session. +/// 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)] -internal sealed class SessionSkillsGetInvokedRequest +public sealed class CompletionsRequestResult { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Completion items in host-ranked order. + [JsonPropertyName("items")] + public IList Items { get => field ??= []; set; } } -/// Name of the skill to enable for the session. +/// Request host-driven completions for the current composer input. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SkillsEnableRequest +internal sealed class CompletionsRequestRequest { - /// Name of the skill to enable. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// 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; } -/// Name of the skill to disable for the session. +/// Instruction sources loaded for the session, in merge order. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SkillsDisableRequest +public sealed class InstructionsGetSourcesResult { - /// 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; + /// 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 SessionSkillsReloadRequest +internal sealed class SessionInstructionsGetSourcesRequest { /// Target session identifier. [JsonPropertyName("sessionId")] @@ -14017,264 +14219,181 @@ internal sealed class SessionSkillsReloadRequest /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSkillsEnsureLoadedRequest +internal sealed class SessionInstructionsReloadRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Recorded MCP server connection failure. +/// Result of one customization reload component. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpServerFailureInfo +public sealed class CustomizationReloadOutcome { - /// Failure message produced when the MCP server connection failed. - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; + /// Reason for a skipped component or description of a failure, when available. + [JsonPropertyName("detail")] + public string? Detail { get; set; } - /// epoch-ms timestamp at which the failure was recorded. - [JsonPropertyName("timestamp")] - public long Timestamp { get; set; } + /// Whether the component reloaded, was skipped, or failed. + [JsonPropertyName("status")] + public CustomizationReloadStatus Status { get; set; } + + /// Component whose reload was attempted or skipped. + [JsonPropertyName("subsystem")] + public CustomizationReloadSubsystem Subsystem { get; set; } } -/// Recorded MCP server pending-auth state. +/// 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 sealed class McpServerNeedsAuthInfo +public sealed class CustomizationsReloadResult { - /// epoch-ms timestamp at which the server signalled it needs authentication. - [JsonPropertyName("timestamp")] - public long Timestamp { get; set; } + /// Errors from any component that could not be refreshed. + [JsonPropertyName("errors")] + public IList Errors { get => field ??= []; set; } + + /// Outcome of each component in reload order; a skipped component was not configured or loaded. + [JsonPropertyName("outcomes")] + public IList Outcomes { get => field ??= []; set; } + + /// Warnings from skill discovery. + [JsonPropertyName("warnings")] + public IList Warnings { get => field ??= []; set; } } -/// Host-level state, omitted when no MCP host is initialized. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpHostState +internal sealed class SessionCustomizationsReloadRequest { - /// Names of currently-connected MCP clients. - [JsonPropertyName("clients")] - public IList Clients { get => field ??= []; set; } - - /// Configured servers that are explicitly disabled. - [JsonPropertyName("disabledServers")] - public IList DisabledServers { get => field ??= []; 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Owned installation that a listed MCP server's live configuration came from. +/// Indicates whether fleet mode was successfully activated. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpServerOwnership +public sealed class FleetStartResult { - /// Stable installation identifier from the owned installation receipt. - [JsonPropertyName("installationId")] - public string InstallationId { get; set; } = string.Empty; + /// Whether fleet mode was successfully activated. + [JsonPropertyName("started")] + public bool Started { get; set; } } -/// MCP server status entry, including config source/plugin source and any connection error. +/// 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 McpServer +internal sealed class FleetStartRequest { - /// Human-readable display name supplied by a managed server catalog. - [JsonPropertyName("displayName")] - public string? DisplayName { 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; } + /// Optional attachments (files, directories, selections, blobs, GitHub references) to include with the fleet request. + [JsonPropertyName("attachments")] + public IList? Attachments { get; set; } - /// Plugin version that provided this server, when source is plugin. - [JsonPropertyName("sourcePluginVersion")] - public string? SourcePluginVersion { 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; } - /// Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured. - [JsonPropertyName("status")] - public McpServerStatus Status { get; set; } -} + /// Optional user prompt to combine with fleet instructions. + [JsonPropertyName("prompt")] + public string? Prompt { 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Configured MCP servers. - [JsonPropertyName("servers")] - public IList Servers { get => field ??= []; 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; } } -/// Identifies the target session. +/// Agents available to the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMcpListRequest +public sealed class AgentList { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Available agents. + [JsonPropertyName("agents")] + public IList Agents { get => field ??= []; set; } } -/// Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block. +/// RPC data type for SessionAgentList operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpToolUi +public sealed class SessionAgentListRequest { - /// 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; } + /// 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; } - /// Tool visibility advertised by the server. When absent, MCP Apps defaults apply. - [JsonPropertyName("visibility")] - public IList? Visibility { 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; } } -/// MCP tool metadata with tool name, optional description, and normalized MCP Apps discovery metadata. +/// RPC data type for SessionAgentListRequestWithSession operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpTools +internal sealed class SessionAgentListRequestWithSession { - /// Tool description, when provided. - [JsonPropertyName("description")] - public string? Description { 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; } - /// Tool name. - [JsonPropertyName("name")] - public string Name { 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; } - /// 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Tools exposed by the connected MCP server. Throws when the server is not connected. +/// An in-memory authored prompt override for an available agent. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpListToolsResult +internal sealed class AgentSetPromptRequest { - /// Tools exposed by the server. - [JsonPropertyName("tools")] - public IList Tools { get => field ??= []; set; } -} + /// Stable effective agent id. Plugin namespace separators are normalized. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; -/// 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; + /// 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; } -/// Name of the MCP server to enable for the session. +/// The currently selected custom agent, or null when using the default agent. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpEnableRequest +public sealed class AgentGetCurrentResult { - /// 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)] - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// Currently selected custom agent, or null if using the default agent. + [JsonPropertyName("agent")] + public AgentInfo? Agent { get; set; } } -/// Name of the MCP server to enable for the session. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpEnableRequestWithSession +internal sealed class SessionAgentGetCurrentRequest { - /// 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)] - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Name of the MCP server to disable for the session. +/// The newly selected custom agent. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpDisableRequest +public sealed class AgentSelectResult { - /// 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 required string ServerName { get; set; } + /// The newly selected custom agent. + [JsonPropertyName("agent")] + public AgentInfo Agent { get => field ??= new(); set; } } -/// Name of the MCP server to disable for the session. +/// Name of the custom agent to select for subsequent turns. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpDisableRequestWithSession +internal sealed class AgentSelectRequest { - /// 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; + /// Name of the custom agent to select. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] @@ -14283,414 +14402,570 @@ internal sealed class McpDisableRequestWithSession /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMcpReloadRequest +internal sealed class SessionAgentDeselectRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Result of moving in-flight MCP loading to the background. +/// Custom agents available to the session after reloading definitions from disk. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MoveMcpLoadingToBackgroundResult +public sealed class AgentReloadResult { - /// 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; } + /// Reloaded custom agents. + [JsonPropertyName("agents")] + public IList Agents { get => field ??= []; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMcpMoveLoadingToBackgroundRequest +internal sealed class SessionAgentReloadRequest { /// 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. +/// Identifier assigned to the newly started background agent task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAllowedServer +public sealed class TasksStartAgentResult { - /// Allowed server name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// PII-free note explaining why the server was allowed. - [JsonPropertyName("redactedNote")] - public string? RedactedNote { get; set; } + /// Generated agent ID for the background task. + [JsonPropertyName("agentId")] + public string AgentId { get; set; } = string.Empty; } -/// MCP server whose connection attempt failed. +/// Agent type, prompt, name, and optional description and model override for the new task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpFailedServer +internal sealed class TasksStartAgentRequest { - /// The captured connection failure detail. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// Type of agent to start (e.g., 'explore', 'task', 'general-purpose'). + [JsonPropertyName("agentType")] + public string AgentType { get; set; } = string.Empty; - /// The config key of the server that failed to connect. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; -} + /// Short description of the task. + [JsonPropertyName("description")] + public string? Description { 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; } + /// Optional model override. + [JsonPropertyName("model")] + public string? Model { get; set; } - /// Filtered server name. + /// Friendly, non-unique name used when displaying the agent. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Human-readable filter reason. - [JsonPropertyName("reason")] - public string Reason { get; set; } = string.Empty; - - /// PII-free filter reason. - [JsonPropertyName("redactedReason")] - public string? RedactedReason { get; set; } -} - -/// MCP server startup filtering result. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpStartServersResult -{ - /// Non-default servers allowed by policy. - [JsonPropertyName("allowedServers")] - public IList? AllowedServers { get; 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; } -} + /// Task prompt for the agent. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; -/// Opaque MCP reload configuration. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpReloadWithConfigRequest -{ /// 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. +/// 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 McpExecuteSamplingResult +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "type", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(TaskInfoAgent), "agent")] +[JsonDerivedType(typeof(TaskInfoClient), "client")] +[JsonDerivedType(typeof(TaskInfoShell), "shell")] +public partial class TaskInfo { + /// The type discriminator. + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; } -/// Outcome of an MCP sampling execution: success result, failure error, or cancellation. + +/// 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 McpSamplingExecutionResult +public partial class TaskInfoAgent : TaskInfo { - /// 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; } + /// + [JsonIgnore] + public override string Type => "agent"; - /// Error description, present when action='failure'. + /// ISO 8601 timestamp when the current active period began. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("activeStartedAt")] + public DateTimeOffset? ActiveStartedAt { get; set; } + + /// Accumulated active execution time in milliseconds. + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("activeTimeMs")] + public TimeSpan? ActiveTime { get; set; } + + /// Type of agent running this task. + [JsonPropertyName("agentType")] + public required string AgentType { 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; } + + /// ISO 8601 timestamp when the task finished. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("completedAt")] + public DateTimeOffset? CompletedAt { get; set; } + + /// Short description of the task. + [JsonPropertyName("description")] + public required string Description { get; set; } + + /// Friendly, non-unique name intended for display. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + /// Error message when the task failed. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [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; } -} + /// Whether task execution is synchronously awaited or managed in the background. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("executionMode")] + public TaskExecutionMode? ExecutionMode { 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)] -public sealed class McpExecuteSamplingRequest -{ -} + /// Unique task identifier. + [JsonPropertyName("id")] + public required string Id { get; set; } -/// 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; } + /// ISO 8601 timestamp when the agent entered idle state. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("idleSince")] + public DateTimeOffset? IdleSince { 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; } + /// Most recent response text from the agent. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("latestResponse")] + public string? LatestResponse { 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; + /// Requested model override for the task when specified. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("model")] + public string? Model { get; set; } - /// Name of the MCP server that initiated the sampling request. - [JsonPropertyName("serverName")] - public string ServerName { 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; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Runtime model resolved for the task when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("resolvedModel")] + public string? ResolvedModel { get; set; } -/// Indicates whether an in-flight sampling execution with the given requestId was found and cancelled. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpCancelSamplingExecutionResult -{ - /// 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; } -} + /// Result text from the task when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("result")] + public string? Result { get; set; } -/// The requestId previously passed to executeSampling that should be cancelled. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpCancelSamplingExecutionParams -{ - /// The requestId previously passed to executeSampling that should be cancelled. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// ISO 8601 timestamp when the task was started. + [JsonPropertyName("startedAt")] + public required DateTimeOffset StartedAt { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Current lifecycle status of the task. + [JsonPropertyName("status")] + public required TaskStatus Status { get; set; } -/// Env-value mode recorded on the session after the update. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpSetEnvValueModeResult -{ - /// Mode recorded on the session after the update. - [JsonPropertyName("mode")] - public McpSetEnvValueModeDetails Mode { get; set; } + /// Tool call ID associated with this agent task. + [JsonPropertyName("toolCallId")] + public required string ToolCallId { get; set; } } -/// Mode controlling how MCP server env values are resolved (`direct` or `indirect`). +/// Public owner attribution for a client-owned task. Identifiers are opaque and never authorize requests. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpSetEnvValueModeParams +public sealed class TaskClientOwner { - /// 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; } + /// ISO 8601 timestamp when the bound join disconnected. + [JsonPropertyName("disconnectedAt")] + public DateTimeOffset? DisconnectedAt { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Display-only owner name. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + /// Opaque identity of the currently or most recently bound session join. + [JsonPropertyName("joinId")] + public string JoinId { get; set; } = string.Empty; + + /// Class of the task owner. + [JsonPropertyName("kind")] + public TaskClientOwnerKind Kind { get; set; } + + /// Opaque session-scoped participant identity. + [JsonPropertyName("participantId")] + public string ParticipantId { get; set; } = string.Empty; + + /// Whether this task's bound join is currently connected. + [JsonPropertyName("presence")] + public TaskClientOwnerPresence Presence { get; set; } + + /// Display-only owner source. + [JsonPropertyName("source")] + public string? Source { get; set; } } -/// Indicates whether the auto-managed `github` MCP server was removed (false when nothing to remove). +/// Tracked client-owned task metadata. +/// The client variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpRemoveGitHubResult +public partial class TaskInfoClient : TaskInfo { - /// 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; } + /// + [JsonIgnore] + public override string Type => "client"; + + /// ISO 8601 timestamp when the current active segment started. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("activeStartedAt")] + public DateTimeOffset? ActiveStartedAt { get; set; } + + /// Accumulated active execution time in milliseconds. + [JsonPropertyName("activeTimeMs")] + public required long ActiveTimeMs { get; set; } + + /// Whether the currently bound owner can receive a cancellation request. + [JsonPropertyName("canCancel")] + public required bool CanCancel { get; set; } + + /// Human-readable reason for terminal cancellation. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("cancellationReason")] + public string? CancellationReason { get; set; } + + /// Owner-scoped registration and reclaim key. + [JsonPropertyName("clientTaskId")] + public required string ClientTaskId { get; set; } + + /// ISO 8601 timestamp when the task reached a terminal status. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("completedAt")] + public DateTimeOffset? CompletedAt { get; set; } + + /// Task description. + [JsonPropertyName("description")] + public required string Description { get; set; } + + /// Optional task display name. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + /// Human-readable terminal failure message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Optional owner-supplied terminal failure code. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("errorCode")] + public string? ErrorCode { get; set; } + + /// Execution mode, which is always background for client-owned tasks. + [JsonPropertyName("executionMode")] + public required TaskClientExecutionMode ExecutionMode { get; set; } + + /// Canonical runtime-generated task identifier. + [JsonPropertyName("id")] + public required string Id { get; set; } + + /// ISO 8601 timestamp when the connected owner entered idle status. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("idleSince")] + public DateTimeOffset? IdleSince { get; set; } + + /// ISO 8601 timestamp of the most recent orphan transition. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("orphanedAt")] + public DateTimeOffset? OrphanedAt { get; set; } + + /// Public attribution and presence for the task owner. + [JsonPropertyName("owner")] + public required TaskClientOwner Owner { get; set; } + + /// ISO 8601 timestamp of the most recent successful reclaim. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reclaimedAt")] + public DateTimeOffset? ReclaimedAt { get; set; } + + /// Opaque successful terminal result supplied by the task owner. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } + + /// Sequence number of the latest accepted owner update. + [JsonPropertyName("sequence")] + public required long Sequence { get; set; } + + /// ISO 8601 timestamp when the task started. + [JsonPropertyName("startedAt")] + public required DateTimeOffset StartedAt { get; set; } + + /// 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; } } -/// Identifies the target 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)] -internal sealed class SessionMcpRemoveGitHubRequest +public partial class TaskInfoShell : TaskInfo { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + [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; } + + /// Whether this shell task can be promoted to background mode. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("canPromoteToBackground")] + public bool? CanPromoteToBackground { get; set; } + + /// Command being executed. + [JsonPropertyName("command")] + public required string Command { get; set; } + + /// ISO 8601 timestamp when the task finished. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("completedAt")] + public DateTimeOffset? CompletedAt { get; set; } + + /// Short description of the task. + [JsonPropertyName("description")] + public required string Description { get; set; } + + /// Whether task execution is synchronously awaited or managed in the background. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("executionMode")] + public TaskExecutionMode? ExecutionMode { get; set; } + + /// Unique task identifier. + [JsonPropertyName("id")] + public required string Id { get; set; } + + /// Path to the detached shell log, when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("logPath")] + public string? LogPath { get; set; } + + /// Process ID when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pid")] + public long? Pid { 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; } } -/// Result of configuring GitHub MCP. +/// Background tasks currently tracked by the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpConfigureGitHubResult +public sealed class TaskList { - /// Whether GitHub MCP configuration changed. - [JsonPropertyName("changed")] - public bool Changed { get; set; } + /// Currently tracked tasks. + [JsonPropertyName("tasks")] + public IList Tasks { get => field ??= []; 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 SessionTasksListRequest { /// 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. +/// Tracked client-owned task metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpStartServerRequest +public sealed class TaskClientInfo { - /// 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; } + /// ISO 8601 timestamp when the current active segment started. + [JsonPropertyName("activeStartedAt")] + public DateTimeOffset? ActiveStartedAt { get; set; } - /// Exact receipt identity for explicit owned activation in this session. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } + /// Accumulated active execution time in milliseconds. + [JsonPropertyName("activeTimeMs")] + public long ActiveTimeMs { get; set; } - /// Name of the MCP server to start. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } -} + /// Whether the currently bound owner can receive a cancellation request. + [JsonPropertyName("canCancel")] + public bool CanCancel { get; set; } -/// 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 McpStartServerRequestWithSession -{ - /// 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; } + /// Human-readable reason for terminal cancellation. + [JsonPropertyName("cancellationReason")] + public string? CancellationReason { get; set; } - /// Exact receipt identity for explicit owned activation in this session. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } + /// Owner-scoped registration and reclaim key. + [JsonPropertyName("clientTaskId")] + public string ClientTaskId { get; set; } = string.Empty; - /// Name of the MCP server to start. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// ISO 8601 timestamp when the task reached a terminal status. + [JsonPropertyName("completedAt")] + public DateTimeOffset? CompletedAt { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Task description. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; -/// 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 McpRestartServerRequest -{ - /// 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 task display name. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - /// Exact receipt identity for an explicit owned restart; configuration overrides are refused. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } + /// Human-readable terminal failure message. + [JsonPropertyName("error")] + public string? Error { get; set; } - /// Name of the MCP server to restart. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } -} + /// Optional owner-supplied terminal failure code. + [JsonPropertyName("errorCode")] + public string? ErrorCode { 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; } + /// Execution mode, which is always background for client-owned tasks. + [JsonPropertyName("executionMode")] + public TaskClientExecutionMode ExecutionMode { get; set; } - /// Exact receipt identity for an explicit owned restart; configuration overrides are refused. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } + /// Canonical runtime-generated task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// Name of the MCP server to restart. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// ISO 8601 timestamp when the connected owner entered idle status. + [JsonPropertyName("idleSince")] + public DateTimeOffset? IdleSince { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// ISO 8601 timestamp of the most recent orphan transition. + [JsonPropertyName("orphanedAt")] + public DateTimeOffset? OrphanedAt { get; set; } -/// Server name for an individual MCP server stop. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpStopServerRequest -{ - /// Exact owned receipt identity. Stop also forgets this session's durable activation. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } + /// Public attribution and presence for the task owner. + [JsonPropertyName("owner")] + public TaskClientOwner Owner { get => field ??= new(); set; } - /// Name of the MCP server to stop. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } -} + /// ISO 8601 timestamp of the most recent successful reclaim. + [JsonPropertyName("reclaimedAt")] + public DateTimeOffset? ReclaimedAt { get; set; } -/// Server name for an individual MCP server stop. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpStopServerRequestWithSession -{ - /// Exact owned receipt identity. Stop also forgets this session's durable activation. - [JsonPropertyName("expectedInstallationId")] - public string? ExpectedInstallationId { get; set; } + /// Opaque successful terminal result supplied by the task owner. + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } - /// Name of the MCP server to stop. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// Sequence number of the latest accepted owner update. + [JsonPropertyName("sequence")] + public long Sequence { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// ISO 8601 timestamp when the task started. + [JsonPropertyName("startedAt")] + public DateTimeOffset StartedAt { get; set; } -/// Registration parameters for an external MCP client. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpRegisterExternalClientRequest -{ - /// Logical server name for the external client. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// Client task lifecycle status. + [JsonPropertyName("status")] + public TaskClientStatus Status { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } } -/// Server name identifying the external client to remove. +/// Result of registering or reclaiming a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpUnregisterExternalClientRequest +public sealed class TasksRegisterResult { - /// Server name of the external client to unregister. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// True only when this invocation created a new task. + [JsonPropertyName("created")] + public bool Created { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// True only when this invocation reclaimed an orphaned task. + [JsonPropertyName("reclaimed")] + public bool Reclaimed { get; set; } -/// Whether the named MCP server is running. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpIsServerRunningResult -{ - /// True if the server has an active client and transport. - [JsonPropertyName("running")] - public bool Running { get; set; } + /// Authoritative registered or reclaimed task. + [JsonPropertyName("task")] + public TaskClientInfo Task { get => field ??= new(); set; } } -/// Server name to check running status for. +/// Registers or reclaims a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpIsServerRunningRequest +internal sealed class TasksRegisterRequest { - /// Name of the MCP server to check. - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + /// 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; } } -/// Indicates whether the pending MCP OAuth response was accepted. +/// Result of publishing a client-owned task update. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpOauthHandlePendingResult +public sealed class TasksUpdateResult { - /// Whether the response was accepted. False if the request was unknown, timed out, or already resolved. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// 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; } } -/// Host response to the pending OAuth request. +/// Progress or terminal update for a client-owned task. /// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(McpOauthPendingRequestResponseToken), "token")] -[JsonDerivedType(typeof(McpOauthPendingRequestResponseCancelled), "cancelled")] -public partial class McpOauthPendingRequestResponse +[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")] @@ -14698,814 +14973,822 @@ public partial class McpOauthPendingRequestResponse } -/// The token variant of . +/// Publishes nonterminal progress for a running or idle client task. +/// The progress variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpOauthPendingRequestResponseToken : McpOauthPendingRequestResponse +public partial class TaskClientUpdateProgress : TaskClientUpdate { /// [JsonIgnore] - public override string Kind => "token"; + public override string Kind => "progress"; - /// Access token acquired by the SDK host. - [JsonPropertyName("accessToken")] - public required string AccessToken { get; set; } + /// Optional progress message appended to recent activity when nonempty. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } - /// Token lifetime in seconds, if known. + /// Optional completion percentage; null clears the current percentage. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("expiresIn")] - public long? ExpiresIn { get; set; } + [JsonPropertyName("percentage")] + public double? Percentage { get; set; } - /// OAuth token type. Defaults to bearer when omitted. + /// Optional progress phase; null clears the current phase. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("tokenType")] - public string? TokenType { get; set; } + [JsonPropertyName("phase")] + public string? Phase { get; set; } + + /// Optional active status transition. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("status")] + public TaskClientActiveStatus? Status { get; set; } } -/// The cancelled variant of . +/// Reports successful terminal completion. +/// The completed variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpOauthPendingRequestResponseCancelled : McpOauthPendingRequestResponse +public partial class TaskClientUpdateCompleted : TaskClientUpdate { /// [JsonIgnore] - public override string Kind => "cancelled"; -} + public override string Kind => "completed"; -/// Pending MCP OAuth request ID and host-provided token or cancellation response. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpOauthHandlePendingRequest -{ - /// OAuth request identifier from the mcp.oauth_required event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Optional final progress message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } - /// Host response to the pending OAuth request. + /// Optional opaque successful terminal result. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("result")] - public McpOauthPendingRequestResponse Result { get => field ??= new(); set; } - - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + public JsonElement? Result { get; set; } } -/// Identifies the MCP server whose persisted OAuth credentials were updated. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpOauthAuthenticationStateChangedRequest +/// Reports terminal failure. +/// The failed variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class TaskClientUpdateFailed : TaskClientUpdate { - /// 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; } + /// + [JsonIgnore] + public override string Kind => "failed"; - /// 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; } + /// Optional owner-supplied terminal failure code. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("code")] + public string? Code { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } } -/// An inert runtime-issued login handle. Preparation alone performs no activation or OAuth work. +/// Reports terminal cancellation after external work stopped. +/// The cancelled variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionMcpOauthPrepareLoginResult +public partial class TaskClientUpdateCancelled : TaskClientUpdate { - /// Original expiry, not extended by consumption, retries or cancellation. - [JsonPropertyName("expiresAt")] - public DateTimeOffset ExpiresAt { get; set; } + /// + [JsonIgnore] + public override string Kind => "cancelled"; - /// Retain with the original requester and use for one login or cancellation. - [JsonPropertyName("loginId")] - public string LoginId { get; set; } = string.Empty; + /// 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; } } -/// Effect-free preparation bound to the existing local session, requester and installation, with frozen options. +/// Updates a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMcpOauthPrepareLoginRequest +internal sealed class TasksUpdateRequest { - /// 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; - - /// Request a new authorisation rather than accepting a usable cached grant. - [JsonPropertyName("forceReauth")] - public bool? ForceReauth { get; set; } + /// Canonical runtime-generated 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; + /// 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; } } -/// OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server. +/// 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 McpOauthLoginResult +public sealed class TasksRefreshResult { - /// 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. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpOauthLoginRequest +internal sealed class SessionTasksRefreshRequest { - /// 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 required string ServerName { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback success-page copy, and static OAuth client selection. +/// 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 McpOauthLoginRequestWithSession +public sealed class TasksWaitForPendingResult { - /// 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; +} +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionTasksWaitForPendingRequest +{ /// 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. +/// 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)] [JsonPolymorphic( - TypeDiscriminatorPropertyName = "status", + TypeDiscriminatorPropertyName = "type", 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 +[JsonDerivedType(typeof(TaskProgressAgent), "agent")] +[JsonDerivedType(typeof(TaskProgressClient), "client")] +[JsonDerivedType(typeof(TaskProgressShell), "shell")] +public partial class TaskProgress { /// The type discriminator. - [JsonPropertyName("status")] - public virtual string Status { get; set; } = string.Empty; + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; } -/// The no-auth-required variant of . +/// Timestamped display line for task progress output or recent agent activity. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpOauthProbeResultNoAuthRequired : McpOauthProbeResult +public sealed class TaskProgressLine { - /// - [JsonIgnore] - public override string Status => "no-auth-required"; + /// Display message, e.g., "▸ bash", "✓ edit src/foo.ts". + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; - /// HTTP response returned by the server. - [JsonPropertyName("httpResponse")] - public required McpOauthHttpResponse HttpResponse { get; set; } + /// ISO 8601 timestamp when this event occurred. + [JsonPropertyName("timestamp")] + public DateTimeOffset Timestamp { get; set; } } -/// The authenticated variant of . +/// 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 partial class McpOauthProbeResultAuthenticated : McpOauthProbeResult +public partial class TaskProgressAgent : TaskProgress { /// [JsonIgnore] - public override string Status => "authenticated"; + public override string Type => "agent"; - /// HTTP response returned by the server. - [JsonPropertyName("httpResponse")] - public required McpOauthHttpResponse HttpResponse { get; set; } + /// The most recent intent reported by the agent. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("latestIntent")] + public string? LatestIntent { get; set; } + + /// Recent tool execution events converted to display lines. + [JsonPropertyName("recentActivity")] + public required IList RecentActivity { get; set; } } -/// The needs-auth variant of . +/// Generic progress for a client-owned task. +/// The client variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpOauthProbeResultNeedsAuth : McpOauthProbeResult +public partial class TaskProgressClient : TaskProgress { /// [JsonIgnore] - public override string Status => "needs-auth"; + public override string Type => "client"; - /// HTTP 401 or 403 response returned by the server. - [JsonPropertyName("httpResponse")] - public required McpOauthHttpResponse HttpResponse { get; set; } + /// Most recent nonempty progress message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("lastMessage")] + public string? LastMessage { get; set; } - /// Why authentication is needed. - [JsonPropertyName("reason")] - public required McpOauthProbeNeedsAuthReason Reason { get; set; } + /// Current completion percentage from zero through one hundred. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("percentage")] + public double? Percentage { get; set; } - /// Parsed WWW-Authenticate challenge parameters, when present and parseable. + /// Current owner-defined progress phase. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("wwwAuthenticateParams")] - public McpOauthWWWAuthenticateParams? WwwAuthenticateParams { get; set; } + [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; } } -/// The failed variant of . +/// 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 partial class McpOauthProbeResultFailed : McpOauthProbeResult +public partial class TaskProgressShell : TaskProgress { /// [JsonIgnore] - public override string Status => "failed"; - - /// Human-readable probe failure detail. - [JsonPropertyName("error")] - public required string Error { get; set; } + public override string Type => "shell"; - /// HTTP response returned by the server, when the probe reached the server and captured the complete response. + /// Process ID when available. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("httpResponse")] - public McpOauthHttpResponse? HttpResponse { get; set; } + [JsonPropertyName("pid")] + public long? Pid { get; set; } + + /// Recent stdout/stderr lines from the running shell command. + [JsonPropertyName("recentOutput")] + public required string RecentOutput { get; set; } } -/// Remote MCP server name for a passive OAuth status probe. +/// Progress information for the task, or null when no task with that ID is tracked. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpOauthProbeRequest +public sealed class TasksGetProgressResult { - /// 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 required string ServerName { get; set; } + /// 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; } } -/// Remote MCP server name for a passive OAuth status probe. +/// Identifier of the background task to fetch progress for. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpOauthProbeRequestWithSession +internal sealed class TasksGetProgressRequest { - /// 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; + /// 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; } -/// Honest terminal cancellation result; persistence or recovery failures remain RPC errors. +/// The first sync-waiting task that can currently be promoted to background mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionMcpOauthCancelLoginResult +public sealed class TasksGetCurrentPromotableResult { - /// True after cancellation settles, false when the original login already connected successfully. - [JsonPropertyName("cancelled")] - public bool Cancelled { get; set; } + /// 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; } } -/// 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 SessionTasksGetCurrentPromotableRequest { - /// 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; } -/// Indicates whether the pending MCP OAuth response was accepted. +/// Indicates whether the task was successfully promoted to background mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpOauthRespondResult +public sealed class TasksPromoteToBackgroundResult { - /// Whether the response was accepted. False if the request was unknown, timed out, or already resolved. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Whether the task was successfully promoted to background mode. + [JsonPropertyName("promoted")] + public bool Promoted { get; set; } } -/// Pending MCP OAuth request id to respond to. +/// Identifier of the task to promote to background mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpOauthRespondRequest +internal sealed class TasksPromoteToBackgroundRequest { - /// OAuth request identifier from the mcp.oauth_required event. - [JsonPropertyName("requestId")] - public string RequestId { 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 headers refresh response was accepted. +/// The promoted task as it now exists in background mode, omitted if no promotable task was waiting. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpHeadersHandlePendingHeadersRefreshRequestResult +public sealed class TasksPromoteCurrentToBackgroundResult { - /// Whether the response was accepted. False if the request was unknown, timed out, or already resolved. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// 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; } } -/// Host response: supply dynamic headers or decline this refresh. -/// Polymorphic base type discriminated by kind. +/// Identifies the target session. [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 +internal sealed class SessionTasksPromoteCurrentToBackgroundRequest { - /// 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; } - -/// The headers variant of . +/// Indicates whether the background task was successfully cancelled. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpHeadersHandlePendingHeadersRefreshRequestHeaders : McpHeadersHandlePendingHeadersRefreshRequest +public sealed class TasksCancelResult { - /// - [JsonIgnore] - public override string Kind => "headers"; - - /// 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; } - - /// 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; } + /// Whether the task was successfully cancelled. + [JsonPropertyName("cancelled")] + public bool Cancelled { get; set; } } -/// The none variant of . +/// Identifier of the background task to cancel. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpHeadersHandlePendingHeadersRefreshRequestNone : McpHeadersHandlePendingHeadersRefreshRequest +internal sealed class TasksCancelRequest { - /// - [JsonIgnore] - public override string Kind => "none"; + /// Task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// The error variant of . +/// 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 partial class McpHeadersHandlePendingHeadersRefreshRequestError : McpHeadersHandlePendingHeadersRefreshRequest +public sealed class TasksRemoveResult { - /// - [JsonIgnore] - public override string Kind => "error"; - - /// Host credential broker failure, denial, or revocation reason. - [JsonPropertyName("message")] - public required string Message { 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; } } -/// MCP headers refresh request id and the host response. +/// Identifier of the completed or cancelled task to remove from tracking. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpHeadersHandlePendingHeadersRefreshRequestRequest +internal sealed class TasksRemoveRequest { - /// 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; } + /// Task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; /// 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. +/// Indicates whether the message was delivered, with an error message when delivery failed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsResourceContent +public sealed class TasksSendMessageResult { - /// Resource-level metadata (CSP, permissions, etc.). - [JsonPropertyName("_meta")] - public IDictionary? Meta { get; set; } - - /// Base64-encoded binary content. - [JsonPropertyName("blob")] - public string? Blob { get; set; } + /// Error message if delivery failed. + [JsonPropertyName("error")] + public string? Error { get; set; } - /// MIME type of the content. - [JsonPropertyName("mimeType")] - public string? MimeType { get; set; } + /// Whether the message was successfully delivered or steered. + [JsonPropertyName("sent")] + public bool Sent { get; set; } +} - /// Text content (e.g. HTML). - [JsonPropertyName("text")] - public string? Text { get; set; } +/// Identifier of the target agent task, message content, and optional sender agent ID. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class TasksSendMessageRequest +{ + /// Agent ID of the sender, if sent on behalf of another agent. + [JsonPropertyName("fromAgentId")] + public string? FromAgentId { get; set; } - /// The resource URI (typically ui://...). - [JsonPropertyName("uri")] - public string Uri { get; set; } = string.Empty; + /// 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; } -/// Resource contents returned by the MCP server. +/// 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 McpAppsReadResourceResult +public sealed class Skill { - /// Resource contents returned by the server. - [JsonPropertyName("contents")] - public IList Contents { get => field ??= []; set; } + /// Optional freeform hint describing the skill's expected arguments, from the `argument-hint` frontmatter field. + [JsonPropertyName("argumentHint")] + public string? ArgumentHint { get; set; } + + /// Canonical slash command name used to invoke the skill, without the leading '/'. + [JsonPropertyName("commandName")] + public string? CommandName { get; set; } + + /// Description of what the skill does. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// Whether the skill is currently enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// Unique identifier for the skill. + [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; } + + /// Whether the skill can be invoked by the user as a slash command. + [JsonPropertyName("userInvocable")] + public bool UserInvocable { get; set; } } -/// MCP server and resource URI to fetch. +/// Skills available to the session, with their enabled state. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpAppsReadResourceRequest +public sealed class SkillList { - /// 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; + /// 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; +} - /// Resource URI (typically ui://...). - [JsonPropertyName("uri")] - public string Uri { get; set; } = string.Empty; +/// Skill invocation record with name, path, content, allowed tools, and turn number. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SkillsInvokedSkill +{ + /// 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; + + /// Whether model invocation was disabled when this skill was invoked. + [JsonPropertyName("disableModelInvocation")] + public bool? DisableModelInvocation { get; set; } + + /// Turn number when the skill was invoked. + [JsonPropertyName("invokedAtTurn")] + public long InvokedAtTurn { get; set; } + + /// Unique identifier for the skill. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// 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; } -/// App-callable tools from the named MCP server. +/// Skills invoked during this session, ordered by invocation time (most recent last). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsListToolsResult +public sealed class SkillsGetInvokedResult { - /// App-callable tools from the server. - [JsonPropertyName("tools")] - public IList> Tools { get => field ??= []; set; } + /// Skills invoked during this session, ordered by invocation time (most recent last). + [JsonPropertyName("skills")] + public IList Skills { get => field ??= []; set; } } -/// MCP server to list app-callable tools for. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpAppsListToolsRequest +internal sealed class SessionSkillsGetInvokedRequest { - /// **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; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { 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; +/// 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; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// MCP server, tool name, and arguments to invoke from an MCP App view. +/// Name of the skill to disable for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpAppsCallToolRequest +internal sealed class SkillsDisableRequest { - /// Tool arguments. - [JsonPropertyName("arguments")] - public IDictionary? Arguments { get; set; } + /// Name of the skill to disable. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// **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; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// 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; +/// 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 SessionSkillsReloadRequest +{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; +} - /// MCP tool name. - [JsonPropertyName("toolName")] - public string ToolName { 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; } -/// Host context advertised to MCP App guests. +/// Recorded MCP server connection failure. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsSetHostContextDetails +public sealed class McpServerFailureInfo { - /// Display modes the host supports. - [JsonPropertyName("availableDisplayModes")] - public IList? AvailableDisplayModes { get; set; } + /// Failure message produced when the MCP server connection failed. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; - /// Current display mode (SEP-1865). - [JsonPropertyName("displayMode")] - public McpAppsSetHostContextDetailsDisplayMode? DisplayMode { get; set; } + /// epoch-ms timestamp at which the failure was recorded. + [JsonPropertyName("timestamp")] + public long Timestamp { get; set; } +} - /// BCP-47 locale, e.g. 'en-US'. - [JsonPropertyName("locale")] - public string? Locale { 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; } +} - /// Platform type for responsive design. - [JsonPropertyName("platform")] - public McpAppsSetHostContextDetailsPlatform? Platform { get; set; } +/// Host-level state, omitted when no MCP host is initialized. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpHostState +{ + /// Names of currently-connected MCP clients. + [JsonPropertyName("clients")] + public IList Clients { get => field ??= []; set; } - /// UI theme preference per SEP-1865. - [JsonPropertyName("theme")] - public McpAppsSetHostContextDetailsTheme? Theme { get; set; } + /// Configured servers that are explicitly disabled. + [JsonPropertyName("disabledServers")] + public IList DisabledServers { get => field ??= []; set; } - /// IANA timezone, e.g. 'America/New_York'. - [JsonPropertyName("timeZone")] - public string? TimeZone { get; set; } + /// Map of server name to recorded connection failure. + [JsonPropertyName("failedServers")] + public IDictionary FailedServers { get => field ??= new Dictionary(); set; } - /// Host application identifier. - [JsonPropertyName("userAgent")] - public string? UserAgent { get; 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; } } -/// Host context to advertise to MCP App guests. +/// Owned installation that a listed MCP server's live configuration came from. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpAppsSetHostContextRequest +public sealed class McpServerOwnership { - /// 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; + /// Stable installation identifier from the owned installation receipt. + [JsonPropertyName("installationId")] + public string InstallationId { get; set; } = string.Empty; } -/// Current host context. +/// MCP server status entry, including config source/plugin source and any connection error. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsHostContextDetails +public sealed class McpServer { - /// Display modes the host supports. - [JsonPropertyName("availableDisplayModes")] - public IList? AvailableDisplayModes { get; set; } + /// Human-readable display name supplied by a managed server catalog. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - /// Current display mode (SEP-1865). - [JsonPropertyName("displayMode")] - public McpAppsHostContextDetailsDisplayMode? DisplayMode { get; set; } + /// Error message if the server failed to connect. + [JsonPropertyName("error")] + public string? Error { get; set; } - /// BCP-47 locale, e.g. 'en-US'. - [JsonPropertyName("locale")] - public string? Locale { 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; - /// Platform type for responsive design. - [JsonPropertyName("platform")] - public McpAppsHostContextDetailsPlatform? Platform { 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; } - /// UI theme preference per SEP-1865. - [JsonPropertyName("theme")] - public McpAppsHostContextDetailsTheme? Theme { 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; } - /// IANA timezone, e.g. 'America/New_York'. - [JsonPropertyName("timeZone")] - public string? TimeZone { get; set; } + /// Configuration source: user, workspace, plugin, builtin, or managed. + [JsonPropertyName("source")] + public McpServerSource? Source { get; set; } - /// Host application identifier. - [JsonPropertyName("userAgent")] - public string? UserAgent { 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; } } -/// Current host context advertised to MCP App guests. +/// MCP servers configured for the session, with their connection status and host-level state. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsHostContext +public sealed class McpServerList { - /// Current host context. - [JsonPropertyName("context")] - public McpAppsHostContextDetails Context { get => field ??= new(); set; } + /// Host-level state, omitted when no MCP host is initialized. + [JsonPropertyName("host")] + public McpHostState? Host { get; 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 SessionMcpAppsGetHostContextRequest +internal sealed class SessionMcpListRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Capability negotiation snapshot. +/// Observational state for a matching already materialized MCP server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsDiagnoseCapability +public sealed class McpConfiguredServerState { - /// Whether the runtime advertises `extensions.io.modelcontextprotocol/ui` to MCP servers. - [JsonPropertyName("advertised")] - public bool Advertised { get; set; } - - /// Whether the MCP_APPS feature flag (or COPILOT_MCP_APPS env override) is on. - [JsonPropertyName("featureFlagEnabled")] - public bool FeatureFlagEnabled { get; set; } + /// Observed connection error, when the materialized server failed. + [JsonPropertyName("error")] + public string? Error { get; set; } - /// Whether the session has the `mcp-apps` capability. - [JsonPropertyName("sessionHasMcpApps")] - public bool SessionHasMcpApps { get; set; } + /// Observed connection status. This is not a configuration or readiness guarantee. + [JsonPropertyName("status")] + public McpServerStatus Status { get; set; } } -/// What the server returned for this session. +/// Effective MCP configuration entry. Configuration enablement is distinct from the optional live observation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsDiagnoseServer +public sealed class McpConfiguredServer { - /// Whether the named server is currently connected. - [JsonPropertyName("connected")] - public bool Connected { get; set; } + /// Human-readable display name supplied by configuration. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - /// Up to 5 tool names with `_meta.ui` for quick inspection. - [JsonPropertyName("sampleToolNames")] - public IList SampleToolNames { get => field ??= []; set; } + /// Whether this configured server is enabled after session configuration and policy filtering. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } - /// Total tools returned by the server's tools/list. - [JsonPropertyName("toolCount")] - public double ToolCount { get; set; } + /// Observed state from an already materialized matching server. Omitted when no live graph has this configured server; it never determines configuration enablement. + [JsonPropertyName("live")] + public McpConfiguredServerState? Live { get; set; } - /// Tools whose `_meta.ui` is populated (resourceUri and/or visibility set). - [JsonPropertyName("toolsWithUiMeta")] - public double ToolsWithUiMeta { 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; } } -/// Diagnostic snapshot of MCP Apps wiring for the named server. +/// Effective MCP configuration with optional live observations from matching already materialized servers. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpAppsDiagnoseResult +public sealed class McpConfiguredServerList { - /// 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; } + /// Effective configured MCP servers. + [JsonPropertyName("servers")] + public IList Servers { get => field ??= []; set; } } -/// MCP server to diagnose MCP Apps wiring for. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpAppsDiagnoseRequest +internal sealed class SessionMcpListConfiguredRequest { - /// 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. +/// Normalized MCP Apps discovery metadata from a tool's `_meta.ui` block. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourceContent +public sealed class McpToolUi { - /// Resource-level metadata (CSP, permissions, etc.). - [JsonPropertyName("_meta")] - public IDictionary? Meta { get; set; } + /// 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; } - /// Base64-encoded binary content. - [JsonPropertyName("blob")] - public string? Blob { get; set; } + /// Tool visibility advertised by the server. When absent, MCP Apps defaults apply. + [JsonPropertyName("visibility")] + public IList? Visibility { get; set; } +} - /// MIME type of the content. - [JsonPropertyName("mimeType")] - public string? MimeType { 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; } - /// Text content (e.g. HTML). - [JsonPropertyName("text")] - public string? Text { get; set; } + /// Tool name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// The resource URI. - [JsonPropertyName("uri")] - public string Uri { 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; } } -/// Resource contents returned by the MCP server. +/// Tools exposed by the connected MCP server. Throws when the server is not connected. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourcesReadResult +public sealed class McpListToolsResult { - /// Resource contents returned by the server. - [JsonPropertyName("contents")] - public IList Contents { get => field ??= []; set; } + /// Tools exposed by the server. + [JsonPropertyName("tools")] + public IList Tools { get => field ??= []; set; } } -/// MCP server and resource URI to fetch. +/// Server name whose tool list should be returned. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpResourcesReadRequest +internal sealed class McpListToolsRequest { - /// Name of the MCP server hosting the resource. + /// 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)] @@ -15515,202 +15798,232 @@ internal sealed class McpResourcesReadRequest /// 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. +/// Name of the MCP server to enable for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourceAnnotations +public sealed class McpEnableRequest { - /// Server-provided non-standard annotation fields preserved from the MCP response. - [JsonPropertyName("additionalProperties")] - public IDictionary? AdditionalProperties { get; set; } + /// Exact receipt identity for explicit owned activation in this session. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Intended audience roles for this resource. - [JsonPropertyName("audience")] - public IList? Audience { 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 required string ServerName { get; set; } +} - /// Last-modified timestamp hint. - [JsonPropertyName("lastModified")] - public string? LastModified { 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; } - /// Priority hint for model/client use. - [JsonPropertyName("priority")] - public double? Priority { 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; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// A resource icon descriptor plus preserved non-standard icon fields. +/// Name of the MCP server to disable for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourceIcon +public sealed class McpDisableRequest { - /// Server-provided non-standard icon fields preserved from the MCP response. - [JsonPropertyName("additionalProperties")] - public IDictionary? AdditionalProperties { get; set; } + /// Required for an owned installation; omission preserves only manual-server behaviour. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Icon MIME type, when known. - [JsonPropertyName("mimeType")] - public string? MimeType { 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; } +} - /// Icon sizes hint. - [JsonPropertyName("sizes")] - public string? Sizes { 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; } - /// Icon URI. - [JsonPropertyName("src")] - public string Src { get; set; } = string.Empty; + /// 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; - /// 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`. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResource +internal sealed class SessionMcpReloadRequest { - /// Resource-level metadata. - [JsonPropertyName("_meta")] - public IDictionary? Meta { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Server-provided non-standard descriptor fields preserved from the MCP response. - [JsonPropertyName("additionalProperties")] - public IDictionary? AdditionalProperties { get; set; } +/// Result of moving in-flight MCP loading to the background. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class MoveMcpLoadingToBackgroundResult +{ + /// 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; } +} - /// Model/client annotations associated with this resource. - [JsonPropertyName("annotations")] - public McpResourceAnnotations? Annotations { get; set; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionMcpMoveLoadingToBackgroundRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Optional description of what this resource represents. - [JsonPropertyName("description")] - public string? Description { get; set; } +/// MCP server allowed by policy, with server name and optional PII-free explanatory note. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpAllowedServer +{ + /// Allowed server name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Icons associated with this resource. - [JsonPropertyName("icons")] - public IList? Icons { get; set; } + /// PII-free note explaining why the server was allowed. + [JsonPropertyName("redactedNote")] + public string? RedactedNote { get; set; } +} - /// MIME type of the resource, if known. - [JsonPropertyName("mimeType")] - public string? MimeType { 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 programmatic name of the resource. + /// The config key of the server that failed to connect. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; +} - /// Resource size in bytes, when known. - [JsonPropertyName("size")] - public long? Size { 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")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] +#endif + [JsonPropertyName("enterpriseName")] + public string? EnterpriseName { get; set; } - /// Optional human-readable display title. - [JsonPropertyName("title")] - public string? Title { get; set; } + /// Filtered server name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// The resource URI (e.g. ui://... or file:///...). - [JsonPropertyName("uri")] - public string Uri { get; set; } = string.Empty; + /// Human-readable filter reason. + [JsonPropertyName("reason")] + public string Reason { get; set; } = string.Empty; + + /// PII-free filter reason. + [JsonPropertyName("redactedReason")] + public string? RedactedReason { get; set; } } -/// One page of resources advertised by the named MCP server. +/// MCP server startup filtering result. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpResourcesListResult +internal sealed class McpStartServersResult { - /// Opaque cursor for the next page, if the server has more resources. - [JsonPropertyName("nextCursor")] - public string? NextCursor { get; set; } + /// Non-default servers allowed by policy. + [JsonPropertyName("allowedServers")] + public IList? AllowedServers { get; set; } - /// Resources advertised by the server (proxied MCP `resources/list`). - [JsonPropertyName("resources")] - public IList Resources { get => field ??= []; 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 whose resources to enumerate. +/// Opaque MCP reload configuration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpResourcesListRequest +internal sealed class McpReloadWithConfigRequest { - /// Opaque MCP pagination cursor from a prior `nextCursor` value. - [JsonPropertyName("cursor")] - public string? Cursor { 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)] - [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] 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`. +/// 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 McpResourceTemplate +public sealed class McpExecuteSamplingResult { - /// 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; } - - /// Optional description of what this template is for. - [JsonPropertyName("description")] - public string? Description { get; set; } - - /// Icons associated with resources matching this template. - [JsonPropertyName("icons")] - public IList? Icons { get; set; } - - /// MIME type for resources matching this template, if uniform. - [JsonPropertyName("mimeType")] - public string? MimeType { get; set; } +} - /// The programmatic name of the resource template. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; +/// Outcome of an MCP sampling execution: success result, failure error, or cancellation. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpSamplingExecutionResult +{ + /// 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; } - /// Optional human-readable display title. - [JsonPropertyName("title")] - public string? Title { get; set; } + /// Error description, present when action='failure'. + [JsonPropertyName("error")] + public string? Error { get; set; } - /// An RFC 6570 URI template for constructing resource URIs. - [JsonPropertyName("uriTemplate")] - public string UriTemplate { 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. + [JsonPropertyName("result")] + public McpExecuteSamplingResult? Result { get; set; } } -/// One page of resource templates advertised by the named MCP server. +/// 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)] -public sealed class McpResourcesListTemplatesResult +public sealed class McpExecuteSamplingRequest { - /// Opaque cursor for the next page, if the server has more resource templates. - [JsonPropertyName("nextCursor")] - public string? NextCursor { get; set; } - - /// Resource templates advertised by the server (proxied MCP `resources/templates/list`). - [JsonPropertyName("resourceTemplates")] - public IList ResourceTemplates { get => field ??= []; set; } } -/// MCP server whose resource templates to enumerate. +/// Identifiers and raw MCP CreateMessageRequest params used to run a sampling inference. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class McpResourcesListTemplatesRequest +internal sealed class McpExecuteSamplingParams { - /// Opaque MCP pagination cursor from a prior `nextCursor` value. - [JsonPropertyName("cursor")] - public string? Cursor { get; set; } + /// 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; } - /// 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)] + /// 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; @@ -15719,355 +16032,258 @@ internal sealed class McpResourcesListTemplatesRequest public string SessionId { get; set; } = string.Empty; } -/// MCP diagnostic source configuration. +/// Indicates whether an in-flight sampling execution with the given requestId was found and cancelled. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpDiagnosticSourceConfiguration +public sealed class McpCancelSamplingExecutionResult { - /// Threshold to apply to MCP diagnostic producers in this session. - [JsonPropertyName("level")] - public DiagnosticLogLevel Level { 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; } } -/// Typed diagnostic source configuration. At least one source is required by diagnostics configuration methods. +/// The requestId previously passed to executeSampling that should be cancelled. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DiagnosticSourcesConfiguration +internal sealed class McpCancelSamplingExecutionParams { - /// MCP diagnostic capture threshold. Omit to leave the current threshold unchanged. - [JsonPropertyName("mcp")] - public McpDiagnosticSourceConfiguration? Mcp { get; set; } + /// The requestId previously passed to executeSampling that should be cancelled. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Per-source session diagnostics configuration. +/// Env-value mode recorded on the session after the update. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class DiagnosticsConfiguration +public sealed class McpSetEnvValueModeResult { - /// Diagnostic thresholds keyed by supported source. - [JsonPropertyName("sources")] - public DiagnosticSourcesConfiguration Sources { get => field ??= new(); set; } + /// Mode recorded on the session after the update. + [JsonPropertyName("mode")] + public McpSetEnvValueModeDetails Mode { get; set; } } -/// Patch session diagnostic thresholds for explicitly supplied sources. +/// Mode controlling how MCP server env values are resolved (`direct` or `indirect`). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class DiagnosticsConfigureRequest +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; } + /// 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; } } -/// 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. +/// Indicates whether the auto-managed `github` MCP server was removed (false when nothing to remove). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "source", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(DiagnosticEntryMcp), "mcp")] -public partial class DiagnosticEntry +public sealed class McpRemoveGitHubResult { - /// The type discriminator. - [JsonPropertyName("source")] - public virtual string Source { get; set; } = string.Empty; + /// 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 +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} -/// MCP-specific detail for a source-discriminated diagnostic entry. +/// Result of configuring GitHub MCP. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class McpDiagnosticDetails +internal sealed class McpConfigureGitHubResult { - /// Fresh identifier for the MCP connection attempt, including failed starts. - [JsonPropertyName("connectionId")] - public string ConnectionId { get; set; } = string.Empty; + /// Whether GitHub MCP configuration changed. + [JsonPropertyName("changed")] + public bool Changed { get; set; } +} - /// Serialized diagnostic detail. Protocol and HTTP records use JSON when detail is present. - [JsonPropertyName("data")] - public string? Data { get; set; } +/// 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; +} - /// Protocol-frame direction when kind is protocol. - [JsonPropertyName("direction")] - public McpDiagnosticDirection? Direction { get; set; } +/// 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 McpStartServerRequest +{ + /// 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; } - /// Diagnostic record category. - [JsonPropertyName("kind")] - public McpDiagnosticKind Kind { get; set; } + /// Exact receipt identity for explicit owned activation in this session. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Configured MCP server name. + /// Name of the MCP server to start. [JsonPropertyName("serverName")] - public string ServerName { get; set; } = string.Empty; + public required string ServerName { get; set; } } -/// The mcp variant of . +/// 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 partial class DiagnosticEntryMcp : DiagnosticEntry +internal sealed class McpStartServerRequestWithSession { - /// - [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; } + /// 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; } - /// Original byte count when a known-size message or data value was truncated. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("originalBytes")] - public long? OriginalBytes { get; set; } + /// Exact receipt identity for explicit owned activation in this session. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// UTC RFC 3339 timestamp captured at the diagnostic source. - [JsonPropertyName("timestamp")] - public required string Timestamp { get; set; } + /// Name of the MCP server to start. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; - /// Whether message or data was truncated to the record-size bound. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("truncated")] - public bool? Truncated { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// One cursor-addressed page of retained session diagnostics. +/// 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 DiagnosticsReadResult +public sealed class McpRestartServerRequest { - /// 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; } + /// 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; } - /// Retained records beginning at the requested cursor. - [JsonPropertyName("entries")] - public IList Entries { get => field ??= []; set; } + /// Exact receipt identity for an explicit owned restart; configuration overrides are refused. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Whether additional retained records follow this page. - [JsonPropertyName("hasMore")] - public bool HasMore { get; set; } + /// Name of the MCP server to restart. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } } -/// Cursor-based request for session diagnostics. The default limit is 100 (maximum 500); the default waitMs is zero (maximum 30000). +/// 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 DiagnosticsReadRequest +internal sealed class McpRestartServerRequestWithSession { - /// Opaque cursor returned by an earlier read. Omit to start at the oldest retained record. - [JsonPropertyName("cursor")] - public string? Cursor { 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; } - /// Maximum number of records to return, from 1 through 500. Omit for 100. - [JsonPropertyName("max")] - public long? Max { get; set; } + /// Exact receipt identity for an explicit owned restart; configuration overrides are refused. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } + + /// Name of the MCP server to restart. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; /// 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; } +/// Server name for an individual MCP server stop. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpStopServerRequest +{ + /// Exact owned receipt identity. Stop also forgets this session's durable activation. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Maximum time in milliseconds to wait for a new record, from 0 through 30000. - [JsonPropertyName("waitMs")] - public int? WaitMs { get; set; } + /// Name of the MCP server to stop. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } } -/// Feature detection and hard polling limits for the EXPERIMENTAL session connector API. +/// Server name for an individual MCP server stop. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorCapabilities +internal sealed class McpStopServerRequestWithSession { - /// Connector API contract version. - [JsonPropertyName("apiVersion")] - public long ApiVersion { get; set; } + /// Exact owned receipt identity. Stop also forgets this session's durable activation. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { 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; } - - /// Maximum accepted wall-clock deadline in milliseconds for one continuation call. - [JsonPropertyName("maxDeadlineMs")] - public long MaxDeadlineMs { get; set; } - - /// Maximum accepted polling attempts for one continuation call. - [JsonPropertyName("maxPollAttempts")] - public long MaxPollAttempts { get; set; } - - /// Maximum accepted delay in milliseconds between polling attempts. - [JsonPropertyName("maxPollIntervalMs")] - public long MaxPollIntervalMs { 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 of the MCP server to stop. + [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. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorAuthorizationRequirement -{ - /// Exact opaque account selection that made the Connector request. - [JsonPropertyName("accountId")] - public string AccountId { get; set; } = string.Empty; - - /// Stable OAuth scope the selected account must grant. - [JsonPropertyName("scope")] - public ConnectorAuthorizationScope Scope { get; set; } -} - -/// Credential-free Connector catalog entry. +/// Registration parameters for an external MCP client. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorCatalogEntry +internal sealed class McpRegisterExternalClientRequest { - /// Untrusted service description, when present. - [JsonPropertyName("description")] - public string? Description { get; set; } - - /// Untrusted display label from the service. - [JsonPropertyName("displayName")] - public string DisplayName { get; set; } = string.Empty; - - /// Canonical Connector name used by lifecycle methods. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// Opaque stable runtime IDs currently projected into the session for this Connector. - [JsonPropertyName("runtimeServerIds")] - public IList RuntimeServerIds { get => field ??= []; set; } + /// Logical server name for the external client. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; - /// Current authoritative service connection state. - [JsonPropertyName("status")] - public ConnectorCatalogStatus Status { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Validated Connector catalog snapshot cached by the session. +/// Server name identifying the external client to remove. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorCatalogResult +internal sealed class McpUnregisterExternalClientRequest { - /// 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; } + /// Server name of the external client to unregister. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; - /// Monotonically increasing session-local catalog revision. - [JsonPropertyName("revision")] - public long Revision { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Live status of one session-owned MCP projection. +/// Whether the named MCP server is running. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorRuntimeStatus +public sealed class McpIsServerRunningResult { - /// Canonical Connector name that owns this server. - [JsonPropertyName("connectorName")] - public string ConnectorName { get; set; } = string.Empty; - - /// Opaque runtime server ID. - [JsonPropertyName("runtimeServerId")] - public string RuntimeServerId { get; set; } = string.Empty; - - /// Current live MCP host status. - [JsonPropertyName("status")] - public ConnectorMcpStatus Status { get; set; } + /// True if the server has an active client and transport. + [JsonPropertyName("running")] + public bool Running { get; set; } } -/// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. +/// Server name to check running status for. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorStatus +internal sealed class McpIsServerRunningRequest { - /// Opaque account selection pinned to this session, when one has been selected. - [JsonPropertyName("accountId")] - public string? AccountId { get; set; } - - /// Connector API contract version. - [JsonPropertyName("apiVersion")] - public long ApiVersion { 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; } - - /// Current feature and session availability. - [JsonPropertyName("availability")] - public ConnectorAvailability Availability { get; set; } - - /// Latest validated catalog snapshot, when available. - [JsonPropertyName("catalog")] - public ConnectorCatalogResult? Catalog { get; set; } - - /// Number of active opaque connection continuations. - [JsonPropertyName("pendingConnections")] - public long PendingConnections { get; set; } - - /// Live MCP status for every Connector-owned runtime server. - [JsonPropertyName("runtimeServers")] - public IList RuntimeServers { get => field ??= []; set; } -} + /// Name of the MCP server to check. + [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. +/// Indicates whether the pending MCP OAuth response was accepted. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ConnectorAccountRequest +public sealed class McpOauthHandlePendingResult { - /// Opaque account selection ID previously returned by an account discovery API. - [JsonPropertyName("accountId")] - public string AccountId { get; set; } = string.Empty; - - /// 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; } } -/// Typed result of initiating or continuing a Connector connection. +/// Host response to the pending OAuth request. /// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "kind", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(ConnectorConnectResultConnected), "connected")] -[JsonDerivedType(typeof(ConnectorConnectResultConsentRequired), "consent_required")] -[JsonDerivedType(typeof(ConnectorConnectResultPending), "pending")] -public partial class ConnectorConnectResult +[JsonDerivedType(typeof(McpOauthPendingRequestResponseToken), "token")] +[JsonDerivedType(typeof(McpOauthPendingRequestResponseCancelled), "cancelled")] +public partial class McpOauthPendingRequestResponse { /// The type discriminator. [JsonPropertyName("kind")] @@ -16075,9403 +16291,11814 @@ public partial class ConnectorConnectResult } -/// The service is connected and the session MCP graph was reconciled. -/// The connected variant of . +/// The token variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ConnectorConnectResultConnected : ConnectorConnectResult +public partial class McpOauthPendingRequestResponseToken : McpOauthPendingRequestResponse { /// [JsonIgnore] - public override string Kind => "connected"; - - /// Fresh authoritative Connector state after MCP reconciliation. - [JsonPropertyName("status")] - public required ConnectorStatus Status { get; set; } -} + public override string Kind => "token"; -/// Host-owned consent is required before bounded continuation can complete. -/// The consent_required variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ConnectorConnectResultConsentRequired : ConnectorConnectResult -{ - /// - [JsonIgnore] - public override string Kind => "consent_required"; + /// Access token acquired by the SDK host. + [JsonPropertyName("accessToken")] + public required string AccessToken { get; set; } - /// Validated HTTPS consent URL. The runtime does not open it. - [JsonPropertyName("consentUrl")] - public required string ConsentUrl { get; set; } + /// Token lifetime in seconds, if known. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("expiresIn")] + public long? ExpiresIn { get; set; } - /// Opaque ID accepted by continueConnection. - [JsonPropertyName("continuationId")] - public required string ContinuationId { get; set; } + /// OAuth token type. Defaults to bearer when omitted. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("tokenType")] + public string? TokenType { get; set; } } -/// The service is still completing the connection without a consent URL. -/// The pending variant of . +/// The cancelled variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ConnectorConnectResultPending : ConnectorConnectResult +public partial class McpOauthPendingRequestResponseCancelled : McpOauthPendingRequestResponse { /// [JsonIgnore] - public override string Kind => "pending"; - - /// Opaque ID accepted by continueConnection. - [JsonPropertyName("continuationId")] - public required string ContinuationId { get; set; } + public override string Kind => "cancelled"; } -/// Selects one Connector and the pinned host-owned account used for its service and MCP authorization. +/// Pending MCP OAuth request ID and host-provided token or cancellation response. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ConnectorConnectRequest +internal sealed class McpOauthHandlePendingRequest { - /// 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; + /// OAuth request identifier from the mcp.oauth_required event. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; - /// Canonical Connector name from the current catalog. - [JsonPropertyName("connectorName")] - public string ConnectorName { get; set; } = string.Empty; + /// Host response to the pending OAuth request. + [JsonPropertyName("result")] + public McpOauthPendingRequestResponse Result { get => field ??= new(); set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Explicitly bounded continuation of a pending Connector connection. +/// Identifies the MCP server whose persisted OAuth credentials were updated. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ConnectorContinueRequest +internal sealed class McpOauthAuthenticationStateChangedRequest { - /// Opaque continuation ID returned by connect, reconnect, or an earlier continuation. - [JsonPropertyName("continuationId")] - public string ContinuationId { get; set; } = string.Empty; - - /// Maximum wall-clock duration in milliseconds for this call. Must be between one and the capability limit. - [JsonPropertyName("deadlineMs")] - public int DeadlineMs { get; set; } - - /// Maximum catalog requests made by this call. Must be between one and the capability limit. - [JsonPropertyName("maxAttempts")] - public int MaxAttempts { 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; } - /// Delay in milliseconds between attempts. Must not exceed the capability limit. - [JsonPropertyName("pollIntervalMs")] - public int PollIntervalMs { 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; } -/// Authoritative result after disconnect and MCP reconciliation. +/// An inert runtime-issued login handle. Preparation alone performs no activation or OAuth work. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ConnectorDisconnectResult +public sealed class SessionMcpOauthPrepareLoginResult { - /// Whether the service accepted the idempotent disconnect. - [JsonPropertyName("disconnected")] - public bool Disconnected { get; set; } + /// Original expiry, not extended by consumption, retries or cancellation. + [JsonPropertyName("expiresAt")] + public DateTimeOffset ExpiresAt { get; set; } - /// Fresh authoritative session state after removing Connector-owned MCP servers. - [JsonPropertyName("status")] - public ConnectorStatus Status { get => field ??= new(); set; } + /// Retain with the original requester and use for one login or cancellation. + [JsonPropertyName("loginId")] + public string LoginId { get; set; } = string.Empty; } -/// Requests authoritative Connector-to-MCP reconciliation for the pinned account. +/// Effect-free preparation bound to the existing local session, requester and installation, with frozen options. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ConnectorReconcileRequest +internal sealed class SessionMcpOauthPrepareLoginRequest { - /// 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; + /// 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; } - /// When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. - [JsonPropertyName("refreshCatalog")] - public bool? RefreshCatalog { 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; + + /// Request a new authorisation rather than accepting a usable cached grant. + [JsonPropertyName("forceReauth")] + public bool? ForceReauth { get; set; } + + /// Name recorded by the authoritative owned installation receipt. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionConnectorsWithdrawProjectionRequest +public sealed class McpOauthLoginResult { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { 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; } + + /// 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; } } -/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +/// 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 ManagedSettingsResolvedData +public sealed class McpOauthLoginRequest { - /// Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true. - [JsonPropertyName("bypassPermissionsDisabled")] - public bool BypassPermissionsDisabled { get; set; } - - /// Whether a session-local permissions layer injected by the SDK host was present. - [JsonPropertyName("clientManaged")] - public bool? ClientManaged { 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; } - /// Whether an actual device MDM/plist/registry/file managed-settings layer was present. - [JsonPropertyName("deviceManaged")] - public bool DeviceManaged { 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; } - /// Whether managed policy could not be determined (e.g. a failed server fetch) and the session fell back to the fail-closed restriction. When true, restrictions such as disabling bypass-permissions are enforced even though `settings` may be absent. - [JsonPropertyName("failClosed")] - public bool FailClosed { 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; } - /// The setting keys under enterprise management in the effective managed settings (e.g. `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force. - [JsonPropertyName("managedKeys")] - public IList ManagedKeys { 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; } - /// Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. - [JsonPropertyName("permissionsAllowIntersected")] - public bool? PermissionsAllowIntersected { get; set; } + /// Exact owned receipt identity. Owned login never uses an implicit helper session. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. - [JsonPropertyName("policyHelperManaged")] - public bool? PolicyHelperManaged { 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; } - /// Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. - [JsonPropertyName("sandboxEnabledByUndeterminedPolicy")] - public bool? SandboxEnabledByUndeterminedPolicy { 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; } - /// Whether the server (account/org) managed-settings layer was present. - [JsonPropertyName("serverManaged")] - public bool ServerManaged { 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; } - /// The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. - [JsonPropertyName("settings")] - public JsonElement? Settings { 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; } - /// Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. - [JsonPropertyName("source")] - public ManagedSettingsResolvedSource Source { 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; } -/// 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; + /// 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; } } -/// Session plugin metadata, with name, marketplace, optional version, and enabled state. +/// 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 Plugin +internal sealed class McpOauthLoginRequestWithSession { - /// Opaque stable identity for a direct plugin source. - [JsonPropertyName("directSourceId")] - public string? DirectSourceId { 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; } - /// Whether the plugin is currently enabled. - [JsonPropertyName("enabled")] - public bool Enabled { 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; } - /// Whether this managed desired plugin has an installed or live record. - [JsonPropertyName("installed")] - public bool? Installed { 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; } - /// Absolute marketplace directory for a live plugin. - [JsonPropertyName("installedFrom")] - public string? InstalledFrom { 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; } - /// Whether enterprise managed settings control this plugin. - [JsonPropertyName("managed")] - public bool? Managed { get; set; } + /// Exact owned receipt identity. Owned login never uses an implicit helper session. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { get; set; } - /// Enabled state required by enterprise managed settings. - [JsonPropertyName("managedDesiredEnabled")] - public bool? ManagedDesiredEnabled { 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; } - /// Marketplace the plugin came from. - [JsonPropertyName("marketplace")] - public string Marketplace { 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; } - /// Plugin name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// + /// 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; } - /// Runtime plugin provenance, such as "builtin". - [JsonPropertyName("source")] - public string? Source { 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; } - /// Installed version. - [JsonPropertyName("version")] - public string? Version { 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; } -/// Plugins installed for the session, with their enabled state and version metadata. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PluginList -{ - /// Installed plugins. - [JsonPropertyName("plugins")] - public IList Plugins { get => field ??= []; 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 SessionPluginsListRequest -{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Plugin source resolved relative to the session's authoritative working directory. +/// Host-delivered callback for a runtime-managed MCP OAuth login. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPluginsInstallRequest +internal sealed class McpOauthCompleteRequest { + /// 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; - - /// 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. +/// 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)] -internal sealed class PluginsUninstallRequestWithSession +[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 { - /// 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; + /// The type discriminator. + [JsonPropertyName("status")] + public virtual string Status { get; set; } = string.Empty; } -/// Name (or spec) of the plugin to update. + +/// The no-auth-required variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsUpdateRequestWithSession +public partial class McpOauthProbeResultNoAuthRequired : McpOauthProbeResult { - /// Plugin name or "plugin@marketplace" spec to update. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Status => "no-auth-required"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// HTTP response returned by the server. + [JsonPropertyName("httpResponse")] + public required McpOauthHttpResponse HttpResponse { get; set; } } -/// Plugin names (or specs) to enable in the session's authoritative working directory. +/// The authenticated variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPluginsEnableRequest +public partial class McpOauthProbeResultAuthenticated : McpOauthProbeResult { - /// 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; } + /// + [JsonIgnore] + public override string Status => "authenticated"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// HTTP response returned by the server. + [JsonPropertyName("httpResponse")] + public required McpOauthHttpResponse HttpResponse { get; set; } } -/// Plugin names (or specs) to disable in the session's authoritative working directory. +/// The needs-auth variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPluginsDisableRequest +public partial class McpOauthProbeResultNeedsAuth : McpOauthProbeResult { - /// 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; } + /// + [JsonIgnore] + public override string Status => "needs-auth"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } } -/// RPC data type for SessionPluginsReload operations. +/// The failed variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionPluginsReloadRequest +public partial class McpOauthProbeResultFailed : McpOauthProbeResult { - /// 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; } - - /// Re-run custom-agent discovery after refreshing plugins. Defaults to true. - [JsonPropertyName("reloadCustomAgents")] - public bool? ReloadCustomAgents { 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; } + /// + [JsonIgnore] + public override string Status => "failed"; - /// 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; } + /// Human-readable probe failure detail. + [JsonPropertyName("error")] + public required string Error { get; set; } - /// Reload MCP server connections after refreshing plugins. Defaults to true. - [JsonPropertyName("reloadMcp")] - public bool? ReloadMcp { 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; } } -/// RPC data type for SessionPluginsReloadRequestWithSession operations. +/// Remote MCP server name for a passive OAuth status probe. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPluginsReloadRequestWithSession +public sealed class McpOauthProbeRequest { - /// 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; } - - /// Re-run custom-agent discovery after refreshing plugins. Defaults to true. - [JsonPropertyName("reloadCustomAgents")] - public bool? ReloadCustomAgents { get; set; } + /// Exact owned receipt identity; probing never activates a dormant installation. + [JsonPropertyName("expectedInstallationId")] + public string? ExpectedInstallationId { 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; } + /// 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; } +} - /// 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; } +/// 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; } - /// Reload MCP server connections after refreshing plugins. Defaults to true. - [JsonPropertyName("reloadMcp")] - public bool? ReloadMcp { 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; } -/// Identifies the target session. +/// Honest terminal cancellation result; persistence or recovery failures remain RPC errors. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPluginsMarketplacesListRequest +public sealed class SessionMcpOauthCancelLoginResult { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// True after cancellation settles, false when the original login already connected successfully. + [JsonPropertyName("cancelled")] + public bool Cancelled { get; set; } } -/// Marketplace source and optional working directory for relative-path resolution. +/// Targets only the original prepared/applying owned login on this exact session requester. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsMarketplacesAddRequestWithSession +internal sealed class SessionMcpOauthCancelLoginRequest { + /// 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; - - /// 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; - - /// 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; } } -/// Name of the marketplace to remove and an optional force flag. +/// Indicates whether the pending MCP OAuth response was accepted. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsMarketplacesRemoveRequestWithSession +public sealed class McpOauthRespondResult { - /// 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; } + /// Whether the response was accepted. False if the request was unknown, timed out, or already resolved. + [JsonPropertyName("success")] + public bool Success { get; set; } +} - /// Marketplace name to remove. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; +/// Pending MCP OAuth request id to respond to. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +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; } -/// Name of the marketplace whose plugin catalog to fetch. +/// Indicates whether the pending MCP headers refresh response was accepted. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PluginsMarketplacesBrowseRequestWithSession +public sealed class McpHeadersHandlePendingHeadersRefreshRequestResult { - /// Marketplace name to browse. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// 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; } } -/// RPC data type for SessionPluginsMarketplacesRefresh operations. +/// Host response: supply dynamic headers or decline this refresh. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionPluginsMarketplacesRefreshRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(McpHeadersHandlePendingHeadersRefreshRequestHeaders), "headers")] +[JsonDerivedType(typeof(McpHeadersHandlePendingHeadersRefreshRequestNone), "none")] +[JsonDerivedType(typeof(McpHeadersHandlePendingHeadersRefreshRequestError), "error")] +public partial class McpHeadersHandlePendingHeadersRefreshRequest { - /// Marketplace name to refresh. When omitted, every registered marketplace is refreshed. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// RPC data type for SessionPluginsMarketplacesRefreshRequestWithSession operations. + +/// The headers variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionPluginsMarketplacesRefreshRequestWithSession +public partial class McpHeadersHandlePendingHeadersRefreshRequestHeaders : McpHeadersHandlePendingHeadersRefreshRequest { - /// Marketplace name to refresh. When omitted, every registered marketplace is refreshed. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// + [JsonIgnore] + public override string Kind => "headers"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } + + /// 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; } } -/// 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. +/// The none variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderSessionToken +public partial class McpHeadersHandlePendingHeadersRefreshRequestNone : McpHeadersHandlePendingHeadersRefreshRequest { - /// 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; + /// + [JsonIgnore] + public override string Kind => "none"; +} - /// 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; } +/// The error variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class McpHeadersHandlePendingHeadersRefreshRequestError : McpHeadersHandlePendingHeadersRefreshRequest +{ + /// + [JsonIgnore] + public override string Kind => "error"; - /// The short-lived token value. - [JsonPropertyName("token")] - public string Token { get; set; } = string.Empty; + /// Host credential broker failure, denial, or revocation reason. + [JsonPropertyName("message")] + public required string Message { get; set; } } -/// A snapshot of the provider endpoint the session is currently configured to talk to. +/// MCP headers refresh request id and the host response. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderEndpoint +internal sealed class McpHeadersHandlePendingHeadersRefreshRequestRequest { - /// A credential the caller should use with this endpoint. Omitted only when the endpoint accepts unauthenticated requests. - [JsonPropertyName("apiKey")] - public string? ApiKey { get; set; } + /// Headers refresh request identifier from mcp.headers_refresh_required. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; - /// Base URL to pass to the LLM client library. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("baseUrl")] - public string BaseUrl { get; set; } = string.Empty; + /// Host response: supply dynamic headers or decline this refresh. + [JsonPropertyName("result")] + public McpHeadersHandlePendingHeadersRefreshRequest Result { get => field ??= new(); set; } - /// HTTP headers the caller must include on every outbound request. - [JsonPropertyName("headers")] - public IDictionary Headers { get => field ??= new Dictionary(); 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. - [JsonPropertyName("sessionToken")] - public ProviderSessionToken? SessionToken { get; set; } +/// MCP Apps resource content with URI, optional MIME type, text or base64 blob, and resource metadata. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpAppsResourceContent +{ + /// Resource-level metadata (CSP, permissions, etc.). + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Transport to be used for provider requests. - [JsonPropertyName("transport")] - public ProviderEndpointTransport? Transport { get; set; } + /// Base64-encoded binary content. + [JsonPropertyName("blob")] + public string? Blob { get; set; } - /// Provider family. Matches the `type` field of a BYOK provider config. - [JsonPropertyName("type")] - public ProviderEndpointType Type { get; set; } + /// MIME type of the content. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } - /// Wire API to be used, when required for the provider type. - [JsonPropertyName("wireApi")] - public ProviderEndpointWireApi? WireApi { 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; } -/// RPC data type for SessionProviderGetEndpoint operations. +/// Resource contents returned by the MCP server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionProviderGetEndpointRequest +public sealed class McpAppsReadResourceResult { - /// 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; } + /// Resource contents returned by the server. + [JsonPropertyName("contents")] + public IList Contents { get => field ??= []; set; } } -/// RPC data type for SessionProviderGetEndpointRequestWithSession operations. +/// MCP server and resource URI to fetch. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionProviderGetEndpointRequestWithSession +internal sealed class McpAppsReadResourceRequest { - /// 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; } + /// 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; } -/// The selectable model entries synthesized for the models added by this call. +/// App-callable tools from the named MCP server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderAddResult +public sealed class McpAppsListToolsResult { - /// 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; } + /// App-callable tools from the server. + [JsonPropertyName("tools")] + public IList> Tools { get => field ??= []; set; } } -/// RPC data type for ProtocolSystemMessageAppendConfig operations. +/// MCP server to list app-callable tools for. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProtocolSystemMessageAppendConfig +internal sealed class McpAppsListToolsRequest { - /// Text appended to the standard system prompt. - [JsonPropertyName("content")] - public string? Content { 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; - /// Append-mode discriminator. Omission also selects append mode. - [JsonPropertyName("mode")] - public ProtocolAppendMode? Mode { get; set; } + /// 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 SystemMessageBlock operations. +/// MCP server, tool name, and arguments to invoke from an MCP App view. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SystemMessageBlock +internal sealed class McpAppsCallToolRequest { - /// Whether providers with explicit prompt caching should place a cache breakpoint after this block. - [JsonPropertyName("cacheBreakpoint")] - public bool? CacheBreakpoint { get; set; } + /// Tool arguments. + [JsonPropertyName("arguments")] + public IDictionary? Arguments { get; set; } - /// Text content for this system-message block. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// **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; - /// Whether the block is static and may be cached independently of dynamic prompt content. - [JsonPropertyName("isStatic")] - public bool? IsStatic { 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; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// MCP tool name. + [JsonPropertyName("toolName")] + public string ToolName { get; set; } = string.Empty; } -/// RPC data type for ProtocolSystemMessageReplaceConfig operations. +/// Host context advertised to MCP App guests. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProtocolSystemMessageReplaceConfig +public sealed class McpAppsSetHostContextDetails { - /// Complete replacement system-message text. - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; + /// Display modes the host supports. + [JsonPropertyName("availableDisplayModes")] + public IList? AvailableDisplayModes { get; set; } - /// Optional structured blocks corresponding to the replacement content. - [JsonPropertyName("contentBlocks")] - public IList? ContentBlocks { get; set; } + /// Current display mode (SEP-1865). + [JsonPropertyName("displayMode")] + public McpAppsSetHostContextDetailsDisplayMode? DisplayMode { get; set; } - /// Replace-mode discriminator. - [JsonPropertyName("mode")] - public ProtocolReplaceMode Mode { 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; } + + /// Host application identifier. + [JsonPropertyName("userAgent")] + public string? UserAgent { get; set; } } -/// RPC data type for ProtocolStaticSectionOverride operations. +/// Host context to advertise to MCP App guests. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProtocolStaticSectionOverride +internal sealed class McpAppsSetHostContextRequest { - /// Declarative operation applied to the section. - [JsonPropertyName("action")] - public ProtocolStaticSectionAction Action { get; set; } + /// Host context advertised to MCP App guests. + [JsonPropertyName("context")] + public McpAppsSetHostContextDetails Context { get => field ??= new(); set; } - /// Optional content used by replace, append, and prepend operations. - [JsonPropertyName("content")] - public string? Content { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Polymorphic base type discriminated by action. +/// Current host context. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonPolymorphic( - TypeDiscriminatorPropertyName = "action", - UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(ProtocolMarkerSectionOverrideTransform), "transform")] -[JsonDerivedType(typeof(ProtocolMarkerSectionOverridePreserve), "preserve")] -public partial class ProtocolMarkerSectionOverride +public sealed class McpAppsHostContextDetails { - /// The type discriminator. - [JsonPropertyName("action")] - public virtual string Action { get; set; } = string.Empty; -} + /// Display modes the host supports. + [JsonPropertyName("availableDisplayModes")] + public IList? AvailableDisplayModes { get; set; } + /// Current display mode (SEP-1865). + [JsonPropertyName("displayMode")] + public McpAppsHostContextDetailsDisplayMode? DisplayMode { get; set; } -/// The transform variant of . + /// 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; } +} + +/// Current host context advertised to MCP App guests. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ProtocolMarkerSectionOverrideTransform : ProtocolMarkerSectionOverride +public sealed class McpAppsHostContext { - /// - [JsonIgnore] - public override string Action => "transform"; + /// Current host context. + [JsonPropertyName("context")] + public McpAppsHostContextDetails Context { get => field ??= new(); set; } } -/// The preserve variant of . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ProtocolMarkerSectionOverridePreserve : ProtocolMarkerSectionOverride +internal sealed class SessionMcpAppsGetHostContextRequest { - /// - [JsonIgnore] - public override string Action => "preserve"; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// JSON union data type for ProtocolSectionOverride. +/// Capability negotiation snapshot. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -public sealed partial class ProtocolSectionOverride +public sealed class McpAppsDiagnoseCapability { - /// Gets the value when this instance contains . - public ProtocolStaticSectionOverride? ProtocolStaticSectionOverride { get; } - - /// Gets the value when this instance contains . - public ProtocolMarkerSectionOverride? ProtocolMarkerSectionOverride { get; } - - /// Initializes a new instance of the class from . - public ProtocolSectionOverride(ProtocolStaticSectionOverride value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolStaticSectionOverride = value; - } - - /// Converts to . - public static implicit operator ProtocolSectionOverride(ProtocolStaticSectionOverride value) => new(value); - - /// Initializes a new instance of the class from . - public ProtocolSectionOverride(ProtocolMarkerSectionOverride value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolMarkerSectionOverride = value; - } + /// Whether the runtime advertises `extensions.io.modelcontextprotocol/ui` to MCP servers. + [JsonPropertyName("advertised")] + public bool Advertised { get; set; } - /// Converts to . - public static implicit operator ProtocolSectionOverride(ProtocolMarkerSectionOverride value) => new(value); + /// Whether the MCP_APPS feature flag (or COPILOT_MCP_APPS env override) is on. + [JsonPropertyName("featureFlagEnabled")] + public bool FeatureFlagEnabled { 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."); - } + /// Whether the session has the `mcp-apps` capability. + [JsonPropertyName("sessionHasMcpApps")] + public bool SessionHasMcpApps { 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); - } +/// What the server returned for this session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpAppsDiagnoseServer +{ + /// Whether the named server is currently connected. + [JsonPropertyName("connected")] + public bool Connected { get; set; } - throw new JsonException("JSON value did not match any ProtocolSectionOverride variant."); - } + /// Up to 5 tool names with `_meta.ui` for quick inspection. + [JsonPropertyName("sampleToolNames")] + public IList SampleToolNames { get => field ??= []; 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; - } + /// Total tools returned by the server's tools/list. + [JsonPropertyName("toolCount")] + public double ToolCount { get; set; } - throw new JsonException("No ProtocolSectionOverride variant value is set."); - } - } + /// Tools whose `_meta.ui` is populated (resourceUri and/or visibility set). + [JsonPropertyName("toolsWithUiMeta")] + public double ToolsWithUiMeta { get; set; } } -/// RPC data type for ProtocolSystemMessageCustomizeConfig operations. +/// Diagnostic snapshot of MCP Apps wiring for the named server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProtocolSystemMessageCustomizeConfig +public sealed class McpAppsDiagnoseResult { - /// Text appended after the customized sections. - [JsonPropertyName("content")] - public string? Content { get; set; } - - /// Customize-mode discriminator. - [JsonPropertyName("mode")] - public ProtocolCustomizeMode Mode { get; set; } + /// Capability negotiation snapshot. + [JsonPropertyName("capability")] + public McpAppsDiagnoseCapability Capability { get => field ??= new(); set; } - /// Named standard-prompt section overrides. - [JsonPropertyName("sections")] - public IDictionary? Sections { get; set; } + /// What the server returned for this session. + [JsonPropertyName("server")] + public McpAppsDiagnoseServer Server { get => field ??= new(); set; } } -/// JSON union data type for ProtocolSystemMessageConfig. +/// MCP server to diagnose MCP Apps wiring for. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -public sealed partial class ProtocolSystemMessageConfig +internal sealed class McpAppsDiagnoseRequest { - /// Gets the value when this instance contains . - public ProtocolSystemMessageAppendConfig? ProtocolSystemMessageAppendConfig { get; } - - /// Gets the value when this instance contains . - public ProtocolSystemMessageReplaceConfig? ProtocolSystemMessageReplaceConfig { get; } - - /// Gets the value when this instance contains . - public ProtocolSystemMessageCustomizeConfig? ProtocolSystemMessageCustomizeConfig { get; } - - /// Initializes a new instance of the class from . - public ProtocolSystemMessageConfig(ProtocolSystemMessageAppendConfig value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolSystemMessageAppendConfig = value; - } - - /// Converts to . - public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageAppendConfig value) => new(value); - - /// Initializes a new instance of the class from . - public ProtocolSystemMessageConfig(ProtocolSystemMessageReplaceConfig value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolSystemMessageReplaceConfig = value; - } - - /// Converts to . - public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageReplaceConfig value) => new(value); + /// 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; - /// Initializes a new instance of the class from . - public ProtocolSystemMessageConfig(ProtocolSystemMessageCustomizeConfig value) - { - ArgumentNullException.ThrowIfNull(value); - ProtocolSystemMessageCustomizeConfig = value; - } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Converts to . - public static implicit operator ProtocolSystemMessageConfig(ProtocolSystemMessageCustomizeConfig value) => new(value); +/// MCP resource content with URI, optional MIME type, text or base64 blob, and resource metadata. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpResourceContent +{ + /// Resource-level metadata (CSP, permissions, etc.). + [JsonPropertyName("_meta")] + public IDictionary? Meta { 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."); - } + /// Base64-encoded binary content. + [JsonPropertyName("blob")] + public string? Blob { get; set; } - 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); - } + /// MIME type of the content. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } - throw new JsonException("JSON value did not match any ProtocolSystemMessageConfig variant."); - } + /// Text content (e.g. HTML). + [JsonPropertyName("text")] + public string? Text { get; set; } - /// - 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; - } + /// The resource URI. + [JsonPropertyName("uri")] + public string Uri { get; set; } = string.Empty; +} - throw new JsonException("No ProtocolSystemMessageConfig variant value is set."); - } - } +/// Resource contents returned by the MCP server. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpResourcesReadResult +{ + /// Resource contents returned by the server. + [JsonPropertyName("contents")] + public IList Contents { get => field ??= []; set; } } -/// A BYOK model definition referencing a named provider. +/// MCP server and resource URI to fetch. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderModelConfig +internal sealed class McpResourcesReadRequest { - /// Optional capability overrides (vision, tool_calls, reasoning, etc.). - [JsonPropertyName("capabilities")] - public ModelCapabilitiesOverride? Capabilities { 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; - /// 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; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Maximum context window tokens for the model. - [JsonPropertyName("maxContextWindowTokens")] - public double? MaxContextWindowTokens { get; set; } + /// Resource URI. + [JsonPropertyName("uri")] + public string Uri { get; set; } = string.Empty; +} - /// Maximum output tokens for the model. - [JsonPropertyName("maxOutputTokens")] - public double? MaxOutputTokens { get; set; } +/// Standard MCP resource annotations plus preserved non-standard annotation fields. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpResourceAnnotations +{ + /// Server-provided non-standard annotation fields preserved from the MCP response. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Maximum prompt/input tokens for the model. - [JsonPropertyName("maxPromptTokens")] - public double? MaxPromptTokens { get; set; } + /// Intended audience roles for this resource. + [JsonPropertyName("audience")] + public IList? Audience { get; set; } - /// Provider-published model metadata, preserved verbatim as the public Model.metadata object. - [JsonPropertyName("metadata")] - public IDictionary? Metadata { get; set; } + /// Last-modified timestamp hint. + [JsonPropertyName("lastModified")] + public string? LastModified { get; set; } - /// Well-known base model id used for behavior/capability/config lookup. Defaults to `id`. - [JsonPropertyName("modelId")] - public string? ModelId { get; set; } + /// Priority hint for model/client use. + [JsonPropertyName("priority")] + public double? Priority { get; set; } +} - /// Display name for model pickers. Defaults to the provider-qualified selection id (`provider/id`). - [JsonPropertyName("name")] - public string? Name { get; set; } +/// A resource icon descriptor plus preserved non-standard icon fields. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpResourceIcon +{ + /// Server-provided non-standard icon fields preserved from the MCP response. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Name of the configured provider that serves this model. - [JsonPropertyName("provider")] - public string Provider { get; set; } = string.Empty; + /// Icon MIME type, when known. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } - /// 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; } + /// Icon sizes hint. + [JsonPropertyName("sizes")] + public string? Sizes { get; set; } - /// The model name sent to the provider API for inference. Defaults to `id`. - [JsonPropertyName("wireModel")] - public string? WireModel { get; set; } -} + /// Icon URI. + [JsonPropertyName("src")] + public string Src { get; set; } = string.Empty; -/// Azure-specific provider options. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderConfigAzure -{ - /// API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. - [JsonPropertyName("apiVersion")] - public string? ApiVersion { get; set; } + /// 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 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 sealed class NamedProviderConfig +public sealed class McpResource { - /// Static API key used to authenticate provider requests. - [JsonPropertyName("apiKey")] - public string? ApiKey { get; set; } + /// Resource-level metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Azure authentication configuration for the provider. - [JsonPropertyName("azure")] - public ProviderConfigAzure? Azure { get; set; } + /// Server-provided non-standard descriptor fields preserved from the MCP response. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Base URL for provider API requests. - [JsonPropertyName("baseUrl")] - public string BaseUrl { get; set; } = string.Empty; + /// Model/client annotations associated with this resource. + [JsonPropertyName("annotations")] + public McpResourceAnnotations? Annotations { get; set; } - /// Static bearer token used to authenticate provider requests. - [JsonPropertyName("bearerToken")] - public string? BearerToken { get; set; } + /// Optional description of what this resource represents. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Whether the host supplies bearer tokens dynamically. - [JsonPropertyName("hasBearerTokenProvider")] - public bool? HasBearerTokenProvider { get; set; } + /// Icons associated with this resource. + [JsonPropertyName("icons")] + public IList? Icons { get; set; } - /// Additional HTTP headers included with provider requests. - [JsonPropertyName("headers")] - public IDictionary? Headers { get; set; } + /// MIME type of the resource, if known. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } - /// Unique provider name used to qualify model selection IDs. + /// The programmatic name of the resource. [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; } -} - -/// 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)] -internal sealed class ProviderAddRequest -{ - /// 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; } + /// Resource size in bytes, when known. + [JsonPropertyName("size")] + public long? Size { 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; } + /// Optional human-readable display title. + [JsonPropertyName("title")] + public string? Title { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// The resource URI (e.g. ui://... or file:///...). + [JsonPropertyName("uri")] + public string Uri { get; set; } = string.Empty; } -/// The selectable model entries and selection ids synthesized for the synchronized BYOK models. +/// One page of resources advertised by the named MCP server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderSyncResult +public sealed class McpResourcesListResult { - /// 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; } - - /// Synthesized selectable model entries for the synchronized BYOK models. - [JsonPropertyName("models")] - public IList Models { get => field ??= []; set; } + /// Opaque cursor for the next page, if the server has more resources. + [JsonPropertyName("nextCursor")] + public string? NextCursor { get; set; } - /// Provider-qualified model selection ids present after synchronization. - [JsonPropertyName("selectionIds")] - public IList SelectionIds { get => field ??= []; set; } + /// Resources advertised by the server (proxied MCP `resources/list`). + [JsonPropertyName("resources")] + public IList Resources { get => field ??= []; set; } } -/// Authoritative BYOK provider and model registry snapshot to apply atomically to the session. +/// MCP server whose resources to enumerate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ProviderSyncRequest +internal sealed class McpResourcesListRequest { - /// BYOK model definition snapshot. Models absent from this list are removed. - [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 connection snapshot. Providers absent from this list are removed. - [JsonPropertyName("providers")] - public IList? Providers { 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)] + [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. +/// 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 SessionUpdateOptionsResult +public sealed class McpResourceTemplate { - /// Number of hooks loaded from installed plugins, returned when installedPlugins is updated. - [JsonPropertyName("pluginHookCount")] - public long? PluginHookCount { get; set; } + /// Resource-template-level metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } -} + /// Server-provided non-standard descriptor fields preserved from the MCP response. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } -/// Source descriptor for a `session.options.update` content-exclusion rule, with source name and type. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class OptionsUpdateAdditionalContentExclusionPolicyRuleSource -{ - /// Name of the policy source. + /// Model/client annotations associated with this template. + [JsonPropertyName("annotations")] + public McpResourceAnnotations? Annotations { get; set; } + + /// Optional description of what this template is for. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Icons associated with resources matching this template. + [JsonPropertyName("icons")] + public IList? Icons { get; set; } + + /// MIME type for resources matching this template, if uniform. + [JsonPropertyName("mimeType")] + public string? MimeType { get; set; } + + /// The programmatic name of the resource template. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Type of the policy source. - [JsonPropertyName("type")] - public string Type { get; set; } = string.Empty; + /// Optional human-readable display title. + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// An RFC 6570 URI template for constructing resource URIs. + [JsonPropertyName("uriTemplate")] + public string UriTemplate { get; set; } = string.Empty; } -/// Single content-exclusion rule supplied to `session.options.update`, with paths, match conditions, and source. +/// One page of resource templates advertised by the named MCP server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class OptionsUpdateAdditionalContentExclusionPolicyRule +public sealed class McpResourcesListTemplatesResult { - /// Conditions of which at least one must match. - [JsonPropertyName("ifAnyMatch")] - public IList? IfAnyMatch { get; set; } + /// Opaque cursor for the next page, if the server has more resource templates. + [JsonPropertyName("nextCursor")] + public string? NextCursor { get; set; } - /// Conditions none of which may match. - [JsonPropertyName("ifNoneMatch")] - public IList? IfNoneMatch { get; set; } + /// Resource templates advertised by the server (proxied MCP `resources/templates/list`). + [JsonPropertyName("resourceTemplates")] + public IList ResourceTemplates { get => field ??= []; set; } +} - /// Path patterns covered by this rule. - [JsonPropertyName("paths")] - public IList Paths { get => field ??= []; set; } +/// MCP server whose resource templates to enumerate. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpResourcesListTemplatesRequest +{ + /// Opaque MCP pagination cursor from a prior `nextCursor` value. + [JsonPropertyName("cursor")] + public string? Cursor { 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; } + /// 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; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Content-exclusion policy supplied to `session.options.update`, with rules, last-updated data, and scope. +/// An argument accepted by an MCP prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class OptionsUpdateAdditionalContentExclusionPolicy +public sealed class McpPromptArgument { - /// Opaque policy update timestamp supplied by the host. - [JsonPropertyName("last_updated_at")] - public JsonElement LastUpdatedAt { get; set; } + /// Argument-level metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Content-exclusion rules to apply. - [JsonPropertyName("rules")] - public IList Rules { get => field ??= []; set; } + /// Server-provided non-standard argument fields. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Allowed values for the `OptionsUpdateAdditionalContentExclusionPolicyScope` enumeration. - [JsonPropertyName("scope")] - public OptionsUpdateAdditionalContentExclusionPolicyScope Scope { get; set; } + /// Description of the argument. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Name of the argument. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Whether the argument is required; omission is distinct from false. + [JsonPropertyName("required")] + public bool? Required { get; set; } } -/// Options scoped to the built-in CAPI (Copilot API) provider. +/// An MCP prompt icon with standard size hints and preserved non-standard fields. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CapiSessionOptions +public sealed class McpPromptIcon { - /// 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; } + /// Server-provided non-standard icon fields. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { 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; } + /// 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; } } -/// Installed plugin record for a session, with marketplace, version, install time, enabled state, cache path, and source. +/// An MCP prompt descriptor. Server-provided non-standard fields are exposed under `additionalProperties`. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionInstalledPlugin +public sealed class McpPrompt { - /// Path where the plugin is cached locally. - [JsonPropertyName("cache_path")] - public string? CachePath { get; set; } + /// Prompt-level metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Whether the plugin is currently enabled. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// Server-provided non-standard descriptor fields. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Installation timestamp (ISO-8601). - [JsonPropertyName("installed_at")] - public string InstalledAt { get; set; } = string.Empty; + /// Arguments accepted by the prompt. + [JsonPropertyName("arguments")] + public IList? Arguments { 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; } + /// Description of what this prompt provides. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Marketplace the plugin came from (empty string for direct repo installs). - [JsonPropertyName("marketplace")] - public string Marketplace { get; set; } = string.Empty; + /// Icons associated with this prompt. + [JsonPropertyName("icons")] + public IList? Icons { get; set; } - /// Plugin name. + /// The programmatic name of the prompt. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Source descriptor for direct repo installs (when marketplace is empty). - [JsonPropertyName("source")] - public JsonElement? Source { get; set; } + /// Human-readable display title. + [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; } +/// 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; } - /// Installed version, if known. - [JsonPropertyName("version")] - public string? Version { 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; } } -/// Custom model-provider configuration (BYOK). +/// MCP server whose prompts to enumerate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProviderConfig +internal sealed class McpPromptsListRequest { - /// API key. Optional for local providers like Ollama. - [JsonPropertyName("apiKey")] - public string? ApiKey { get; set; } - - /// Azure-specific provider options. - [JsonPropertyName("azure")] - public ProviderConfigAzure? Azure { get; set; } + /// Opaque MCP pagination cursor from a prior `nextCursor` value. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } - /// API endpoint URL. - [JsonPropertyName("baseUrl")] - public string BaseUrl { get; set; } = string.Empty; + /// 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; - /// Bearer token for authentication. Sets the Authorization header directly. Takes precedence over apiKey when both are set. - [JsonPropertyName("bearerToken")] - public string? BearerToken { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { 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; } +/// An MCP prompt message with opaque JSON content preserved without flattening or content-type filtering. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpPromptMessage +{ + /// Message-level metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Custom HTTP headers to include in all outbound requests to the provider. - [JsonPropertyName("headers")] - public IDictionary? Headers { get; set; } + /// Server-provided non-standard message fields. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { get; set; } - /// Maximum context window tokens for the model. - [JsonPropertyName("maxContextWindowTokens")] - public double? MaxContextWindowTokens { get; set; } + /// The original MCP content block, including nested metadata and unfamiliar content types. + [JsonPropertyName("content")] + public JsonElement Content { get; set; } - /// Maximum output tokens for the model. - [JsonPropertyName("maxOutputTokens")] - public double? MaxOutputTokens { get; set; } + /// The role of the message sender. + [JsonPropertyName("role")] + public McpPromptRole Role { get; set; } +} - /// Maximum prompt/input tokens for the model. - [JsonPropertyName("maxPromptTokens")] - public double? MaxPromptTokens { get; set; } +/// Prompt messages returned by the MCP server without sending them to the model. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class McpPromptsGetResult +{ + /// MCP result metadata. + [JsonPropertyName("_meta")] + public IDictionary? Meta { get; set; } - /// Overrides for model capabilities when they cannot be inferred from modelId. - [JsonPropertyName("modelCapabilities")] - public ModelCapabilitiesOverride? ModelCapabilities { get; set; } + /// Server-provided non-standard result fields. + [JsonPropertyName("additionalProperties")] + public IDictionary? AdditionalProperties { 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; } + /// Description of the prompt. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Provider name used for model and telemetry attribution. - [JsonPropertyName("providerName")] - public string? ProviderName { get; set; } + /// Ordered prompt messages. + [JsonPropertyName("messages")] + public IList Messages { get => field ??= []; set; } +} - /// Provider transport. Defaults to "http". - [JsonPropertyName("transport")] - public ProviderConfigTransport? Transport { get; set; } +/// MCP server, prompt name, and optional string-valued arguments. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class McpPromptsGetRequest +{ + /// String-valued arguments to pass to the prompt. + [JsonPropertyName("arguments")] + public IDictionary? Arguments { get; set; } - /// Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. - [JsonPropertyName("type")] - public ProviderConfigType? Type { get; 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; - /// Wire API format (openai/azure only). Defaults to "completions". - [JsonPropertyName("wireApi")] - public ProviderConfigWireApi? WireApi { get; 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; - /// 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// 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. +/// MCP diagnostic source configuration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigAuth +public sealed class McpDiagnosticSourceConfiguration { - /// 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; } + /// Threshold to apply to MCP diagnostic producers in this session. + [JsonPropertyName("level")] + public DiagnosticLogLevel Level { get; set; } } -/// Destinations authorized to receive one masked environment credential. +/// Typed diagnostic source configuration. At least one source is required by diagnostics configuration methods. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxMaskedEnvVar +public sealed class DiagnosticSourcesConfiguration { - /// 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; } + /// MCP diagnostic capture threshold. Omit to leave the current threshold unchanged. + [JsonPropertyName("mcp")] + public McpDiagnosticSourceConfiguration? Mcp { get; set; } } -/// Whole-value environment credential masking for sandboxed children. +/// Per-source session diagnostics configuration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxCredentialsConfig +public sealed class DiagnosticsConfiguration { - /// 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; } + /// Diagnostic thresholds keyed by supported source. + [JsonPropertyName("sources")] + public DiagnosticSourcesConfiguration Sources { get => field ??= new(); set; } } -/// macOS seatbelt experimental options. +/// Patch session diagnostic thresholds for explicitly supplied sources. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyExperimentalSeatbelt +internal sealed class DiagnosticsConfigureRequest { - /// Whether the macOS seatbelt profile may access the keychain. - [JsonPropertyName("keychainAccess")] - public bool? KeychainAccess { get; set; } + /// 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; } } -/// Platform-specific experimental policy fields. +/// 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)] -public sealed class SandboxConfigUserPolicyExperimental +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "source", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(DiagnosticEntryMcp), "mcp")] +public partial class DiagnosticEntry { - /// macOS seatbelt experimental options. - [JsonPropertyName("seatbelt")] - public SandboxConfigUserPolicyExperimentalSeatbelt? Seatbelt { get; set; } + /// The type discriminator. + [JsonPropertyName("source")] + public virtual string Source { get; set; } = string.Empty; } -/// Filesystem rules to merge into the base policy. + +/// MCP-specific detail for a source-discriminated diagnostic entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyFilesystem +public sealed class McpDiagnosticDetails { - /// Whether to clear the policy when the session exits. - [JsonPropertyName("clearPolicyOnExit")] - public bool? ClearPolicyOnExit { get; set; } + /// Fresh identifier for the MCP connection attempt, including failed starts. + [JsonPropertyName("connectionId")] + public string ConnectionId { get; set; } = string.Empty; - /// Paths explicitly denied. - [JsonPropertyName("deniedPaths")] - public IList? DeniedPaths { get; set; } + /// Serialized diagnostic detail. Protocol and HTTP records use JSON when detail is present. + [JsonPropertyName("data")] + public string? Data { get; set; } - /// Paths granted read-only access. - [JsonPropertyName("readonlyPaths")] - public IList? ReadonlyPaths { get; set; } + /// Protocol-frame direction when kind is protocol. + [JsonPropertyName("direction")] + public McpDiagnosticDirection? Direction { get; set; } - /// Paths granted read/write access. - [JsonPropertyName("readwritePaths")] - public IList? ReadwritePaths { get; set; } + /// Diagnostic record category. + [JsonPropertyName("kind")] + public McpDiagnosticKind Kind { get; set; } + + /// Configured MCP server name. + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; } -/// HTTP proxy configuration for sandboxed traffic. +/// The mcp variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicyNetworkProxy +public partial class DiagnosticEntryMcp : DiagnosticEntry { - /// 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; } + /// + [JsonIgnore] + public override string Source => "mcp"; - /// 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; + /// Agent identifier for a subagent host. Omitted for the root agent. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("agentId")] + public string? AgentId { get; set; } - /// 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; } -} + /// Typed MCP diagnostic detail. + [JsonPropertyName("details")] + public required McpDiagnosticDetails Details { 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; } + /// Severity of this emitted diagnostic record. + [JsonPropertyName("level")] + public required DiagnosticSeverity Level { get; set; } - /// Whether traffic to local/loopback addresses is allowed. - [JsonPropertyName("allowLocalNetwork")] - public bool? AllowLocalNetwork { get; set; } + /// Human-readable diagnostic summary. + [JsonPropertyName("message")] + public required string Message { get; set; } - /// Whether outbound network traffic is allowed at all. - [JsonPropertyName("allowOutbound")] - public bool? AllowOutbound { 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; } - /// 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; } + /// UTC RFC 3339 timestamp captured at the diagnostic source. + [JsonPropertyName("timestamp")] + public required string Timestamp { 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; } + /// Whether message or data was truncated to the record-size bound. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("truncated")] + public bool? Truncated { get; set; } } -/// macOS seatbelt-specific options. +/// One cursor-addressed page of retained session diagnostics. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfigUserPolicySeatbelt +public sealed class DiagnosticsReadResult { - /// Whether the macOS seatbelt profile may access the keychain. - [JsonPropertyName("keychainAccess")] - public bool? KeychainAccess { get; set; } + /// 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; } + + /// Retained records beginning at the requested cursor. + [JsonPropertyName("entries")] + public IList Entries { get => field ??= []; set; } + + /// Whether additional retained records follow this page. + [JsonPropertyName("hasMore")] + public bool HasMore { get; set; } } -/// User-managed sandbox policy fragment merged into the auto-discovered base policy. +/// 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)] -public sealed class SandboxConfigUserPolicy +internal sealed class DiagnosticsReadRequest { - /// Deprecated legacy location for `seatbelt`; read only when the top-level `seatbelt` is absent. - [JsonPropertyName("experimental")] - public SandboxConfigUserPolicyExperimental? Experimental { get; set; } + /// Opaque cursor returned by an earlier read. Omit to start at the oldest retained record. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } - /// Filesystem rules to merge into the base policy. - [JsonPropertyName("filesystem")] - public SandboxConfigUserPolicyFilesystem? Filesystem { get; set; } + /// Maximum number of records to return, from 1 through 500. Omit for 100. + [JsonPropertyName("max")] + public long? Max { get; set; } - /// Network rules to merge into the base policy. - [JsonPropertyName("network")] - public SandboxConfigUserPolicyNetwork? Network { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// macOS seatbelt options to merge into the base policy. - [JsonPropertyName("seatbelt")] - public SandboxConfigUserPolicySeatbelt? Seatbelt { get; set; } + /// Nonempty selection of sources to read. Each source may be listed once. + [JsonPropertyName("sources")] + public IList Sources { get => field ??= []; set; } + + /// Maximum time in milliseconds to wait for a new record, from 0 through 30000. + [JsonPropertyName("waitMs")] + public int? WaitMs { get; set; } } -/// Resolved sandbox configuration. +/// Feature detection and hard polling limits for the EXPERIMENTAL session connector API. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SandboxConfig +public sealed class ConnectorCapabilities { - /// Whether to auto-add the current working directory to readwritePaths. Default: true. - [JsonPropertyName("addCurrentWorkingDirectory")] - public bool? AddCurrentWorkingDirectory { get; set; } + /// Connector API contract version. + [JsonPropertyName("apiVersion")] + public long ApiVersion { 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; } + /// Current session availability. Disabled availability is reported without making a Connector request. + [JsonPropertyName("availability")] + public ConnectorAvailability Availability { 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; } + /// Whether connect and reconnect can return an opaque continuation for bounded consent polling. + [JsonPropertyName("consentContinuation")] + public bool ConsentContinuation { get; set; } - /// Credential-injection capability flags. - [JsonPropertyName("auth")] - public SandboxConfigAuth? Auth { get; set; } + /// Maximum accepted wall-clock deadline in milliseconds for one continuation call. + [JsonPropertyName("maxDeadlineMs")] + public long MaxDeadlineMs { 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; } + /// Maximum accepted polling attempts for one continuation call. + [JsonPropertyName("maxPollAttempts")] + public long MaxPollAttempts { get; set; } - /// The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. - [JsonInclude] - [JsonPropertyName("managedLspRoutingLocked")] - internal bool? ManagedLspRoutingLocked { get; set; } + /// Maximum accepted delay in milliseconds between polling attempts. + [JsonPropertyName("maxPollIntervalMs")] + public long MaxPollIntervalMs { 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 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 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 getAccount is supported. Absence means false. + [JsonPropertyName("sessionAccountSelection")] + public bool? SessionAccountSelection { 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; } + /// Whether reconcile accepts forceConnectorName. Absence means false. + [JsonPropertyName("targetedReconcile")] + public bool? TargetedReconcile { get; set; } +} - /// User-managed sandbox policy fragment merged into the auto-discovered base policy. - [JsonPropertyName("userPolicy")] - public SandboxConfigUserPolicy? UserPolicy { get; set; } +/// 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; } -/// -/// 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. -/// +/// Credential-free identity metadata. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ShellCredentials +public sealed class AuthIdentityMetadata { - /// - /// 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; } + /// Identity host. + [JsonPropertyName("host")] + public string Host { get; set; } = string.Empty; - /// - /// 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; } + /// User login. + [JsonPropertyName("login")] + public string Login { get; set; } = string.Empty; + + /// Authentication type. + [JsonPropertyName("type")] + public AuthInfoType Type { get; set; } } -/// A host-provided script sourced before each built-in shell command when its shell target matches the active shell. +/// Session account selection. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ShellInitScript +public sealed class ConnectorSessionAccount { - /// Path to the script to source. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Opaque session-scoped account selection ID. + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; - /// Built-in shell that may source this script. - [JsonPropertyName("shell")] - public ShellInitScriptShell Shell { get; set; } + /// Credential-free identity metadata. + [JsonPropertyName("authInfo")] + public AuthIdentityMetadata AuthInfo { get => field ??= new(); set; } } -/// Per-session settings for built-in shell tools. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ShellOptions +internal sealed class SessionConnectorsGetAccountRequest { - /// 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// - /// 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; } +/// 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 ConnectorAuthorizationRequirement +{ + /// Exact opaque account selection that made the Connector request. + [JsonPropertyName("accountId")] + public string AccountId { get; set; } = string.Empty; - /// - /// 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; } + /// Stable OAuth scope the selected account must grant. + [JsonPropertyName("scope")] + public ConnectorAuthorizationScope Scope { get; set; } } -/// Patch of mutable session options to apply to the running session. +/// Credential-free Connector catalog entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionUpdateOptionsParams +public sealed class ConnectorCatalogEntry { - /// 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; } + /// Untrusted service description, when present. + [JsonPropertyName("description")] + public string? Description { get; set; } - /// Runtime context discriminator (e.g., `cli`, `actions`). - [JsonPropertyName("agentContext")] - public string? AgentContext { get; set; } + /// Untrusted display label from the service. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; - /// Whether to include instructions from every MCP server in the system prompt instead of only allowlisted servers. - [JsonPropertyName("allowAllMcpServerInstructions")] - public bool? AllowAllMcpServerInstructions { get; set; } + /// Optional catalog logo. + [JsonPropertyName("logo")] + public string? Logo { get; set; } - /// Whether to disable the `ask_user` tool (encourages autonomous behavior). - [JsonPropertyName("askUserDisabled")] - public bool? AskUserDisabled { get; set; } + /// Canonical Connector name used by lifecycle methods. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Allowlist of tool names available to this session. - [JsonPropertyName("availableTools")] - public IList? AvailableTools { get; set; } + /// Optional catalog release tag. + [JsonPropertyName("releaseTag")] + public string? ReleaseTag { get; set; } - /// Options scoped to the built-in CAPI (Copilot API) provider. - [JsonPropertyName("capi")] - public CapiSessionOptions? Capi { get; set; } + /// Opaque stable runtime IDs currently projected into the session for this Connector. + [JsonPropertyName("runtimeServerIds")] + public IList RuntimeServerIds { get => field ??= []; set; } - /// Identifier of the client driving the session. - [JsonPropertyName("clientName")] - public string? ClientName { get; set; } + /// Current authoritative service connection state. + [JsonPropertyName("status")] + public ConnectorCatalogStatus Status { get; set; } - /// Whether to include the `Co-authored-by` trailer in commit messages. - [JsonPropertyName("coauthorEnabled")] - public bool? CoauthorEnabled { get; set; } + /// Optional catalog tier. + [JsonPropertyName("tier")] + public string? Tier { 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; } +/// 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; } - /// Whether to allow auto-mode continuation across turns. - [JsonPropertyName("continueOnAutoMode")] - public bool? ContinueOnAutoMode { get; set; } + /// Unix epoch milliseconds when this snapshot was accepted. + [JsonPropertyName("refreshedAtMs")] + public long RefreshedAtMs { get; set; } - /// Override URL for the Copilot API endpoint. - [JsonPropertyName("copilotUrl")] - public string? CopilotUrl { get; set; } + /// Monotonically increasing session-local catalog revision. + [JsonPropertyName("revision")] + public long Revision { get; set; } +} - /// Whether to default custom agents to local-only execution. - [JsonPropertyName("customAgentsLocalOnly")] - public bool? CustomAgentsLocalOnly { 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; - /// Instruction source IDs to exclude from the system prompt. - [JsonPropertyName("disabledInstructionSources")] - public IList? DisabledInstructionSources { get; set; } + /// Opaque runtime server ID. + [JsonPropertyName("runtimeServerId")] + public string RuntimeServerId { get; set; } = string.Empty; - /// Skill IDs that should be excluded from this session. - [JsonPropertyName("disabledSkills")] - public IList? DisabledSkills { get; set; } + /// Current live MCP host status. + [JsonPropertyName("status")] + public ConnectorMcpStatus Status { 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; } +/// Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ConnectorStatus +{ + /// Opaque account selection pinned to this session, when one has been selected. + [JsonPropertyName("accountId")] + public string? AccountId { 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; } + /// Connector API contract version. + [JsonPropertyName("apiVersion")] + public long ApiVersion { 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; } + /// Exact selected account and stable scope requiring an authorization update, when proven by the Connector service. + [JsonPropertyName("authorizationRequirement")] + public ConnectorAuthorizationRequirement? AuthorizationRequirement { get; set; } - /// Whether to surface reasoning-summary events from the model. - [JsonPropertyName("enableReasoningSummaries")] - public bool? EnableReasoningSummaries { get; set; } + /// Current feature and session availability. + [JsonPropertyName("availability")] + public ConnectorAvailability Availability { get; set; } - /// Whether shell-script safety heuristics are enabled. - [JsonPropertyName("enableScriptSafety")] - public bool? EnableScriptSafety { get; set; } + /// Latest validated catalog snapshot, when available. + [JsonPropertyName("catalog")] + public ConnectorCatalogResult? Catalog { get; set; } - /// Whether to enable cross-session store writes and reads. - [JsonPropertyName("enableSessionStore")] - public bool? EnableSessionStore { get; set; } + /// Number of active opaque connection continuations. + [JsonPropertyName("pendingConnections")] + public long PendingConnections { 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; } + /// Live MCP status for every Connector-owned runtime server. + [JsonPropertyName("runtimeServers")] + public IList RuntimeServers { get => field ??= []; set; } +} - /// Whether to stream model responses. - [JsonPropertyName("enableStreaming")] - public bool? EnableStreaming { 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; +} - /// How env values are passed to MCP servers (`direct` inlines literal values; `indirect` resolves at launch). - [JsonPropertyName("envValueMode")] - public OptionsUpdateEnvValueMode? EnvValueMode { 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; - /// 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Whether subagent callback events should be forwarded into the session event log sink. - [JsonPropertyName("eventsLogIncludesSubagents")] - public bool? EventsLogIncludesSubagents { get; set; } +/// Typed result of initiating or continuing a Connector connection. +/// Polymorphic base type discriminated by kind. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(ConnectorConnectResultConnected), "connected")] +[JsonDerivedType(typeof(ConnectorConnectResultConsentRequired), "consent_required")] +[JsonDerivedType(typeof(ConnectorConnectResultPending), "pending")] +public partial class ConnectorConnectResult +{ + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} - /// 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; } +/// The service is connected and the session MCP graph was reconciled. +/// The connected variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class ConnectorConnectResultConnected : ConnectorConnectResult +{ + /// + [JsonIgnore] + public override string Kind => "connected"; - /// Map of feature-flag IDs to their boolean enabled state. - [JsonPropertyName("featureFlags")] - public IDictionary? FeatureFlags { get; set; } + /// Fresh authoritative Connector state after MCP reconciliation. + [JsonPropertyName("status")] + public required ConnectorStatus Status { get; set; } +} - /// Skill scan directories and descendants excluded from discovery. Supports `~`-relative paths. - [JsonPropertyName("ignoredSkillsLocations")] - public IList? IgnoredSkillsLocations { get; set; } +/// Host-owned consent is required before bounded continuation can complete. +/// The consent_required variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class ConnectorConnectResultConsentRequired : ConnectorConnectResult +{ + /// + [JsonIgnore] + public override string Kind => "consent_required"; - /// 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; } + /// Validated HTTPS consent URL. The runtime does not open it. + [JsonPropertyName("consentUrl")] + public required string ConsentUrl { 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; } + /// Opaque ID accepted by continueConnection. + [JsonPropertyName("continuationId")] + public required string ContinuationId { 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; } +/// The service is still completing the connection without a consent URL. +/// The pending variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class ConnectorConnectResultPending : ConnectorConnectResult +{ + /// + [JsonIgnore] + public override string Kind => "pending"; - /// Stable integration identifier used for analytics and rate-limit attribution. - [JsonPropertyName("integrationId")] - public string? IntegrationId { get; set; } + /// Opaque ID accepted by continueConnection. + [JsonPropertyName("continuationId")] + public required string ContinuationId { get; set; } +} - /// Whether experimental capabilities are enabled. - [JsonPropertyName("isExperimentalMode")] - public bool? IsExperimentalMode { 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; - /// Whether interactive shell sessions are logged. - [JsonPropertyName("logInteractiveShells")] - public bool? LogInteractiveShells { get; set; } + /// Canonical Connector name from the current catalog. + [JsonPropertyName("connectorName")] + public string ConnectorName { get; set; } = string.Empty; - /// Identifier sent to LSP-style integrations. - [JsonPropertyName("lspClientName")] - public string? LspClientName { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// 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; } +/// 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; - /// 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; } + /// Maximum wall-clock duration in milliseconds for this call. Must be between one and the capability limit. + [JsonPropertyName("deadlineMs")] + public int DeadlineMs { get; set; } - /// The model ID to use for assistant turns. - [JsonPropertyName("model")] - public string? Model { get; set; } + /// Maximum catalog requests made by this call. Must be between one and the capability limit. + [JsonPropertyName("maxAttempts")] + public int MaxAttempts { get; set; } - /// Per-property model capability overrides for the selected model. - [JsonPropertyName("modelCapabilitiesOverrides")] - public ModelCapabilitiesOverride? ModelCapabilitiesOverrides { get; set; } + /// Delay in milliseconds between attempts. Must not exceed the capability limit. + [JsonPropertyName("pollIntervalMs")] + public int PollIntervalMs { get; set; } - /// Organization-level custom instructions to inject into the system prompt. - [JsonPropertyName("organizationCustomInstructions")] - public string? OrganizationCustomInstructions { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Custom model-provider configuration (BYOK). - [JsonPropertyName("provider")] - public ProviderConfig? Provider { 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; } - /// 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; } + /// Fresh authoritative session state after removing Connector-owned MCP servers. + [JsonPropertyName("status")] + public ConnectorStatus Status { get => field ??= new(); set; } +} - /// Reasoning summary mode for supported model clients. - [JsonPropertyName("reasoningSummary")] - public OptionsUpdateReasoningSummary? ReasoningSummary { 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 the session is running in an interactive UI. - [JsonPropertyName("runningInInteractiveMode")] - public bool? RunningInInteractiveMode { get; set; } + /// Optional Connector name to reinitialize. Requires the targetedReconcile capability. + [JsonPropertyName("forceConnectorName")] + public string? ForceConnectorName { get; set; } - /// Resolved sandbox configuration. - [JsonPropertyName("sandboxConfig")] - public SandboxConfig? SandboxConfig { get; set; } + /// When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. + [JsonPropertyName("refreshCatalog")] + public bool? RefreshCatalog { 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; } +/// 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; - /// 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; } + /// Optional Connector name to reinitialize. Requires the targetedReconcile capability. + [JsonPropertyName("forceConnectorName")] + public string? ForceConnectorName { get; set; } + + /// When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. + [JsonPropertyName("refreshCatalog")] + public bool? RefreshCatalog { 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; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionConnectorsWithdrawProjectionRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// PowerShell process flags applied to built-in and user-requested shell commands. - [JsonPropertyName("shellProcessFlags")] - public IList? ShellProcessFlags { 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; +} - /// Additional directories to search for skills. - [JsonPropertyName("skillDirectories")] - public IList? SkillDirectories { get; set; } +/// Session plugin metadata, with name, marketplace, optional version, and enabled state. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class Plugin +{ + /// Opaque stable identity for a direct plugin source. + [JsonPropertyName("directSourceId")] + public string? DirectSourceId { get; set; } - /// Whether to skip loading custom instruction sources. - [JsonPropertyName("skipCustomInstructions")] - public bool? SkipCustomInstructions { get; set; } + /// Whether the plugin is currently enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } - /// Whether to skip embedding retrieval pipeline initialization and execution. - [JsonPropertyName("skipEmbeddingRetrieval")] - public bool? SkipEmbeddingRetrieval { get; set; } + /// Whether this managed desired plugin has an installed or live record. + [JsonPropertyName("installed")] + public bool? Installed { 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; } + /// Absolute marketplace directory for a live plugin. + [JsonPropertyName("installedFrom")] + public string? InstalledFrom { get; set; } - /// Controls how availableTools (allowlist) and excludedTools (denylist) combine when both are set. - [JsonPropertyName("toolFilterPrecedence")] - public OptionsUpdateToolFilterPrecedence? ToolFilterPrecedence { get; set; } + /// Whether enterprise managed settings control this plugin. + [JsonPropertyName("managed")] + public bool? Managed { get; set; } - /// Optional path for trajectory output. - [JsonPropertyName("trajectoryFile")] - public string? TrajectoryFile { get; set; } + /// Enabled state required by enterprise managed settings. + [JsonPropertyName("managedDesiredEnabled")] + public bool? ManagedDesiredEnabled { get; set; } - /// Output verbosity level for supported models. - [JsonPropertyName("verbosity")] - public Verbosity? Verbosity { get; set; } + /// Marketplace the plugin came from. + [JsonPropertyName("marketplace")] + public string Marketplace { get; set; } = string.Empty; - /// Absolute working-directory path for shell tools. + /// Plugin name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Runtime plugin provenance, such as "builtin". + [JsonPropertyName("source")] + public string? Source { get; set; } + + /// Installed version. + [JsonPropertyName("version")] + public string? Version { get; set; } +} + +/// Plugins installed for the session, with their enabled state and version metadata. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PluginList +{ + /// Installed plugins. + [JsonPropertyName("plugins")] + public IList Plugins { get => field ??= []; set; } +} + +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionPluginsListRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Plugin source resolved relative to the session's authoritative working directory. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionPluginsInstallRequest +{ + /// 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. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PluginsUninstallRequestWithSession +{ + /// 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; +} + +/// Name (or spec) of the plugin to update. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +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; +} + +/// Plugin names (or specs) to enable in the session's authoritative working directory. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionPluginsEnableRequest +{ + /// 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; +} + +/// Plugin names (or specs) to disable in the session's authoritative working directory. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionPluginsDisableRequest +{ + /// 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; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// RPC data type for SessionPluginsReload operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionPluginsReloadRequest +{ + /// 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; } + + /// Re-run custom-agent discovery after refreshing plugins. Defaults to true. + [JsonPropertyName("reloadCustomAgents")] + public bool? ReloadCustomAgents { 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; } + + /// 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; } +} + +/// RPC data type for SessionPluginsReloadRequestWithSession operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionPluginsReloadRequestWithSession +{ + /// 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; } + + /// Re-run custom-agent discovery after refreshing plugins. Defaults to true. + [JsonPropertyName("reloadCustomAgents")] + public bool? ReloadCustomAgents { 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; } + + /// 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; } + + /// 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 SessionPluginsMarketplacesListRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Marketplace source and optional working directory for relative-path resolution. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class PluginsMarketplacesAddRequestWithSession +{ + /// 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; + + /// 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; } } -/// Parameters for (re)loading the merged LSP configuration set. +/// Name of the marketplace to remove and an optional force flag. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class LspInitializeRequest +internal sealed class PluginsMarketplacesRemoveRequestWithSession { - /// Force re-initialization even when LSP configs were already loaded for the working directory. + /// 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; } - /// Git root used as the boundary when traversing for project-level LSP configs (supports monorepos). - [JsonPropertyName("gitRoot")] - public string? GitRoot { get; set; } + /// Marketplace name to remove. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; /// 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; } +/// 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; + + /// 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. +/// RPC data type for SessionPluginsMarketplacesRefresh operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class Extension +public sealed class SessionPluginsMarketplacesRefreshRequest { - /// 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; + /// Marketplace name to refresh. When omitted, every registered marketplace is refreshed. + [JsonPropertyName("name")] + public string? Name { get; set; } +} - /// Extension name (directory name). +/// RPC data type for SessionPluginsMarketplacesRefreshRequestWithSession operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionPluginsMarketplacesRefreshRequestWithSession +{ + /// 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. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProviderSessionToken +{ + /// 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; + + /// 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; } + + /// The short-lived token value. + [JsonPropertyName("token")] + public string Token { get; set; } = string.Empty; +} + +/// A snapshot of the provider endpoint the session is currently configured to talk to. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProviderEndpoint +{ + /// 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; } + + /// 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; } +} + +/// RPC data type for SessionProviderGetEndpoint operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionProviderGetEndpointRequest +{ + /// 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; } +} + +/// RPC data type for SessionProviderGetEndpointRequestWithSession operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionProviderGetEndpointRequestWithSession +{ + /// 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; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// The selectable model entries synthesized for the models added by this call. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProviderAddResult +{ + /// 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; } +} + +/// 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)] +internal sealed class ProviderAddRequest +{ + /// 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; } + + /// 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; } + + /// 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. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProviderSyncResult +{ + /// 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; } + + /// 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; } +} + +/// Authoritative BYOK provider and model registry snapshot to apply atomically to the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ProviderSyncRequest +{ + /// BYOK model definition snapshot. Models absent from this list are removed. + [JsonPropertyName("models")] + public IList? Models { get; set; } + + /// Named BYOK provider connection snapshot. Providers absent from this list are removed. + [JsonPropertyName("providers")] + public IList? Providers { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// What the withdrawal actually removed from the registry. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProviderWithdrawResult +{ + /// 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; } + + /// Selection ids that were registered and are now withdrawn. Excludes requested ids that were not present. + [JsonPropertyName("withdrawn")] + public IList Withdrawn { get => field ??= []; set; } +} + +/// 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; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Indicates whether the session options patch was applied successfully. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionUpdateOptionsResult +{ + /// 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; } +} + +/// Source descriptor for a `session.options.update` content-exclusion rule, with source name and type. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class OptionsUpdateAdditionalContentExclusionPolicyRuleSource +{ + /// Name of the policy source. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Process ID if the extension is running. - [JsonPropertyName("pid")] - public long? Pid { get; set; } + /// Type of the policy source. + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; +} - /// Discovery source: project (.github/extensions/), user (~/.copilot/extensions/), plugin (installed plugin), or session (session-state/<id>/extensions/). +/// Single content-exclusion rule supplied to `session.options.update`, with paths, match conditions, and source. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class OptionsUpdateAdditionalContentExclusionPolicyRule +{ + /// 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.options.update` content-exclusion rule, with source name and type. [JsonPropertyName("source")] - public ExtensionSource Source { get; set; } + public OptionsUpdateAdditionalContentExclusionPolicyRuleSource Source { get => field ??= new(); set; } +} + +/// Content-exclusion policy supplied to `session.options.update`, with rules, last-updated data, and scope. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class OptionsUpdateAdditionalContentExclusionPolicy +{ + /// 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; } + + /// Allowed values for the `OptionsUpdateAdditionalContentExclusionPolicyScope` enumeration. + [JsonPropertyName("scope")] + public OptionsUpdateAdditionalContentExclusionPolicyScope Scope { 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; } + + /// 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; } +} + +/// 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 SessionInstalledPlugin +{ + /// 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 (ISO-8601). + [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 descriptor 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; } + + /// Installed version, if known. + [JsonPropertyName("version")] + public string? Version { get; set; } +} + +/// Custom model-provider configuration (BYOK). +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProviderConfig +{ + /// API key. Optional for local providers like Ollama. + [JsonPropertyName("apiKey")] + public string? ApiKey { get; set; } + + /// Azure-specific provider options. + [JsonPropertyName("azure")] + public ProviderConfigAzure? Azure { get; set; } + + /// API endpoint URL. + [JsonPropertyName("baseUrl")] + public string BaseUrl { 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; } + + /// 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; } + + /// Custom HTTP headers to include in all outbound requests to the provider. + [JsonPropertyName("headers")] + public IDictionary? Headers { get; set; } + + /// Maximum context window tokens for the model. + [JsonPropertyName("maxContextWindowTokens")] + public double? MaxContextWindowTokens { get; set; } + + /// Maximum output tokens for the model. + [JsonPropertyName("maxOutputTokens")] + public double? MaxOutputTokens { get; set; } + + /// Maximum prompt/input tokens for the model. + [JsonPropertyName("maxPromptTokens")] + public double? MaxPromptTokens { get; set; } + + /// Overrides for model capabilities when they cannot be inferred from modelId. + [JsonPropertyName("modelCapabilities")] + public ModelCapabilitiesOverride? ModelCapabilities { 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; } + + /// 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; } + + /// Provider name used for model and telemetry attribution. + [JsonPropertyName("providerName")] + public string? ProviderName { get; set; } + + /// Provider transport. Defaults to "http". + [JsonPropertyName("transport")] + public ProviderConfigTransport? Transport { get; set; } + + /// Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. + [JsonPropertyName("type")] + public ProviderConfigType? Type { 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; } +} + +/// +/// 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 ShellCredentials +{ + /// + /// 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; } +} + +/// 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 ShellInitScript +{ + /// Path to the script to source. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + + /// Built-in shell that may source this script. + [JsonPropertyName("shell")] + public ShellInitScriptShell Shell { get; set; } +} + +/// Per-session settings for built-in shell tools. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ShellOptions +{ + /// 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; } + + /// + /// 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; } + + /// + /// 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; } +} + +/// Patch of mutable session options to apply to the running session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionUpdateOptionsParams +{ + /// 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; } + + /// Organization-level custom instructions to inject into the system prompt. + [JsonPropertyName("organizationCustomInstructions")] + public string? OrganizationCustomInstructions { get; set; } + + /// Custom model-provider configuration (BYOK). + [JsonPropertyName("provider")] + public ProviderConfig? Provider { 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; } + + /// Reasoning summary mode for supported model clients. + [JsonPropertyName("reasoningSummary")] + public OptionsUpdateReasoningSummary? ReasoningSummary { get; set; } + + /// Whether the session is running in an interactive UI. + [JsonPropertyName("runningInInteractiveMode")] + public bool? RunningInInteractiveMode { get; set; } + + /// Resolved sandbox configuration. + [JsonPropertyName("sandboxConfig")] + public SandboxConfig? SandboxConfig { 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; } + + /// 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; } + + /// 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")] +#else + [Obsolete("This member is deprecated and will be removed in a future version.")] +#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; } + + /// Additional directories to search for skills. + [JsonPropertyName("skillDirectories")] + public IList? SkillDirectories { get; set; } + + /// Whether to skip loading custom instruction sources. + [JsonPropertyName("skipCustomInstructions")] + public bool? SkipCustomInstructions { get; set; } + + /// Whether to skip embedding retrieval pipeline initialization and execution. + [JsonPropertyName("skipEmbeddingRetrieval")] + public bool? SkipEmbeddingRetrieval { 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; } +} + +/// Parameters for (re)loading the merged LSP configuration set. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class LspInitializeRequest +{ + /// 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; } +} + +/// Discovered extension metadata, including source-qualified ID, name, discovery source, status, and optional process ID. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class Extension +{ + /// 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; + + /// Extension name (directory 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/). + [JsonPropertyName("source")] + public ExtensionSource Source { get; set; } + + /// Current status: running, disabled, failed, or starting. + [JsonPropertyName("status")] + public ExtensionStatus Status { get; set; } +} + +/// Extensions discovered for the session, with their current status. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +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 SessionExtensionsListRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Source-qualified extension identifier to enable for the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ExtensionsEnableRequest +{ + /// 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; +} + +/// Source-qualified extension identifier to disable for the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ExtensionsDisableRequest +{ + /// 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 SessionExtensionsReloadRequest +{ + /// 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. +[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 +{ + /// The type discriminator. + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; +} + + +/// Optional line range to scope the attachment to a specific section of the file. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PushAttachmentFileLineRange +{ + /// End line number (1-based, inclusive). + [JsonPropertyName("end")] + public long End { get; set; } + + /// Start line number (1-based). + [JsonPropertyName("start")] + public long Start { 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"; + + /// User-facing display name for the attachment. + [JsonPropertyName("displayName")] + public required string DisplayName { 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; } + + /// Absolute file path. + [JsonPropertyName("path")] + public required string Path { 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"; + + /// User-facing display name for the attachment. + [JsonPropertyName("displayName")] + public required string DisplayName { get; set; } + + /// Absolute directory path. + [JsonPropertyName("path")] + public required string Path { 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; } + + /// End line number (0-based). + [JsonPropertyName("line")] + public long Line { 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; } + + /// Start line number (0-based). + [JsonPropertyName("line")] + public long Line { get; set; } +} + +/// Position range of the selection within the file. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PushAttachmentSelectionDetails +{ + /// 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; } +} + +/// Code selection attachment from an editor. +/// The selection variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PushAttachmentSelection : PushAttachment +{ + /// + [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; } + + /// 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; } +} + +/// GitHub issue, pull request, or discussion reference. +/// The github_reference variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PushAttachmentGitHubReference : PushAttachment +{ + /// + [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; } + + /// 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; } +} + +/// Pointer to a GitHub repository. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PushGitHubRepoRef +{ + /// Numeric GitHub repository id. + [JsonPropertyName("id")] + public long? Id { get; set; } + + /// Repository name (without owner). + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Repository owner login (user or organization). + [JsonPropertyName("owner")] + public string Owner { get; set; } = string.Empty; +} + +/// Pointer to a GitHub commit. +/// The github_commit variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PushAttachmentGitHubCommit : PushAttachment +{ + /// + [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; } +} + +/// Pointer to a GitHub release. +/// The github_release variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PushAttachmentGitHubRelease : PushAttachment +{ + /// + [JsonIgnore] + public override string Type => "github_release"; + + /// Human-readable release name. + [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; } + + /// URL to the release on GitHub. + [JsonPropertyName("url")] + public required string Url { get; set; } +} + +/// Pointer to a GitHub Actions job. +/// The github_actions_job variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PushAttachmentGitHubActionsJob : PushAttachment +{ + /// + [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; } + + /// 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; } +} + +/// Pointer to a GitHub repository. +/// The github_repository variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PushAttachmentGitHubRepository : PushAttachment +{ + /// + [JsonIgnore] + public override string Type => "github_repository"; + + /// Short description of the repository. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// 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; } + + /// Repository pointer. + [JsonPropertyName("repo")] + public required PushGitHubRepoRef Repo { get; set; } + + /// URL to the repository on GitHub. + [JsonPropertyName("url")] + public required string Url { get; set; } +} + +/// One side of a file diff (head or base). +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PushAttachmentGitHubFileDiffSide +{ + /// 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; } +} + +/// 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 PushAttachmentGitHubFileDiff : PushAttachment +{ + /// + [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; } + + /// URL to the diff on GitHub (e.g., a commit, compare, or PR-file URL). + [JsonPropertyName("url")] + public required string Url { get; set; } +} + +/// One side of a tree comparison (head or base). +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PushAttachmentGitHubTreeComparisonSide +{ + /// 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; +} + +/// Pointer to a comparison between two git revisions. +/// The github_tree_comparison variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PushAttachmentGitHubTreeComparison : PushAttachment +{ + /// + [JsonIgnore] + public override string Type => "github_tree_comparison"; + + /// Base side of the comparison. + [JsonPropertyName("base")] + public required PushAttachmentGitHubTreeComparisonSide Base { get; set; } + + /// Head side of the comparison. + [JsonPropertyName("head")] + public required PushAttachmentGitHubTreeComparisonSide Head { get; set; } + + /// URL to the comparison on GitHub. + [JsonPropertyName("url")] + public required string Url { get; set; } +} + +/// Generic GitHub URL reference. +/// The github_url variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PushAttachmentGitHubUrl : PushAttachment +{ + /// + [JsonIgnore] + public override string Type => "github_url"; + + /// URL to the GitHub resource. + [JsonPropertyName("url")] + public required string Url { get; set; } +} + +/// 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 PushAttachmentGitHubFile : PushAttachment +{ + /// + [JsonIgnore] + public override string Type => "github_file"; + + /// 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; } + + /// URL to the file on GitHub. + [JsonPropertyName("url")] + public required string Url { get; set; } +} + +/// 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 PushAttachmentGitHubSnippet : PushAttachment +{ + /// + [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; } + + /// Repository the file lives in. + [JsonPropertyName("repo")] + public required PushGitHubRepoRef Repo { get; set; } + + /// URL to the snippet on GitHub (with line anchor). + [JsonPropertyName("url")] + public required string Url { get; set; } +} + +/// Blob attachment with inline base64-encoded data. +/// The blob variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PushAttachmentBlob : PushAttachment +{ + /// + [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; } + + /// MIME type of the inline data. + [JsonPropertyName("mimeType")] + public required string MimeType { get; set; } +} + +/// Slim input shape for extension_context attachments; identity fields are runtime-derived. +/// The extension_context variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PushAttachmentExtensionContext : PushAttachment +{ + /// + [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; } + + /// 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; } +} + +/// Parameters for session.extensions.sendAttachmentsToMessage. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SendAttachmentsToMessageParams +{ + /// 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; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// A tool name and arguments to execute through the session's native invocation pipeline. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ToolsExecuteRequest +{ + /// Arguments supplied to the tool. + [JsonPropertyName("arguments")] + public JsonElement Arguments { 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; + + /// Optional identifier used to correlate this invocation with its tool call. + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } +} + +/// Custom grammar input format accepted by a built-in tool. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class BuiltinToolFormat +{ + /// Grammar definition accepted by the tool. + [JsonPropertyName("definition")] + public string Definition { get; set; } = string.Empty; + + /// Grammar syntax used by the format definition. + [JsonPropertyName("syntax")] + public string Syntax { get; set; } = string.Empty; + + /// Custom input-format discriminator. + [JsonPropertyName("type")] + public BuiltinToolFormatType Type { get; set; } +} + +/// JSON Schema object accepted by a built-in tool. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class BuiltinToolInputSchema +{ + /// Root type of the tool input schema. + [JsonPropertyName("type")] + public BuiltinToolInputSchemaType Type { get; set; } +} + +/// Rust-owned metadata and input schema for a built-in tool. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class BuiltinToolDescriptor +{ + /// 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; } + + /// Whether the tool provides a specialized intention summary. + [JsonPropertyName("hasSummariseIntention")] + public bool HasSummariseIntention { get; set; } + + /// JSON Schema for the tool input, or null when the tool uses a custom format. + [JsonPropertyName("inputSchema")] + public BuiltinToolInputSchema? InputSchema { get; set; } + + /// Optional supplemental usage instructions for the tool. + [JsonPropertyName("instructions")] + public string? Instructions { get; set; } + + /// Whether the tool executes commands in a terminal. + [JsonPropertyName("isTerminal")] + public bool IsTerminal { get; set; } + + /// Stable name used to invoke the built-in tool. + [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; } + + /// 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; } +} + +/// Rust-owned built-in tool descriptors for the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ToolsGetBuiltinDescriptorsResult +{ + /// Built-in tool descriptors materialized for the session. + [JsonPropertyName("tools")] + public IList Tools { get => field ??= []; 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; } + + /// Human-readable shell name. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; + + /// Tool name used to list active shells. + [JsonPropertyName("listShellsToolName")] + public string ListShellsToolName { get; set; } = string.Empty; + + /// Tool name used to read shell output. + [JsonPropertyName("readShellToolName")] + public string ReadShellToolName { get; set; } = string.Empty; + + /// Tool name used to start shell commands. + [JsonPropertyName("shellToolName")] + public string ShellToolName { get; set; } = string.Empty; + + /// Stable shell type identifier. + [JsonPropertyName("shellType")] + public string ShellType { get; set; } = string.Empty; + + /// Tool name used to stop shell commands. + [JsonPropertyName("stopShellToolName")] + public string StopShellToolName { get; set; } = string.Empty; +} + +/// 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; } + + /// 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; + + /// Shell-specific names and description lines for shell tools. + [JsonPropertyName("shellConfig")] + public ToolsShellDescriptorConfig? ShellConfig { get; set; } + + /// Whether the configured shell supports PowerShell 7 syntax. + [JsonPropertyName("shellSupportsPowerShell7Syntax")] + public bool? ShellSupportsPowerShell7Syntax { 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; } +} + +/// Task completion notification with summary from the agent. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class TaskCompleteData +{ + /// 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; } + + /// Summary of the completed task, provided by the agent. + [JsonPropertyName("summary")] + public string? Summary { get; set; } +} + +/// Binary result returned by a tool for the model. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ExternalToolTextResultForLlmBinaryResultsForLlm +{ + /// 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; + + /// Binary result type discriminator. Use "image" for images and "resource" for other binary data. + [JsonPropertyName("type")] + public ExternalToolTextResultForLlmBinaryResultsForLlmType Type { 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. +[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 +{ + /// The type discriminator. + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; +} + + +/// 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"; + + /// The text content. + [JsonPropertyName("text")] + public required string Text { get; set; } +} + +/// Terminal/shell output content block with optional exit code and working directory. +/// The terminal variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class ExternalToolTextResultForLlmContentTerminal : ExternalToolTextResultForLlmContent +{ + /// + [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; } +} + +/// 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"; + + /// 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; } +} + +/// 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"; + + /// Base64-encoded image data. + [Base64String] + [JsonPropertyName("data")] + public required string Data { get; set; } + + /// MIME type of the image (e.g., image/png, image/jpeg). + [JsonPropertyName("mimeType")] + public required string MimeType { 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"; + + /// 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; } +} + +/// 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; } + + /// Available icon sizes (e.g., ['16x16', '32x32']). + [JsonPropertyName("sizes")] + public IList? Sizes { get; set; } + + /// 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; } +} + +/// 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"; + + /// Human-readable description of the resource. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// 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; } +} + +/// 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"; + + /// The embedded resource contents, either text or base64-encoded binary. + [JsonPropertyName("resource")] + public required JsonElement Resource { 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; +} + +/// 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 completion was accepted after the reviewer-rejection budget was exhausted. + [JsonPropertyName("completionRejectionBudgetExhausted")] + public bool? CompletionRejectionBudgetExhausted { get; set; } + + /// 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; } +} + +/// 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; } + + /// Sources returned by the tool that the model may cite. + [JsonPropertyName("citableSources")] + public IList? CitableSources { get; set; } + + /// Structured content blocks returned to the model. + [JsonPropertyName("contents")] + public IList? Contents { get; set; } + + /// Error message for an unsuccessful execution. + [JsonPropertyName("error")] + public string? Error { 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; } +} + +/// 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 ToolsTaskCompleteEventDataRequest +{ + /// 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; } +} + +/// Indicates whether the external tool call result was handled successfully. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class HandlePendingToolCallResult +{ + /// Whether the tool call result was handled successfully. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// 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; } + + /// 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; +} + +/// 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 ToolsInitializeAndValidateResult +{ +} + +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionToolsInitializeAndValidateRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Lightweight metadata for a currently initialized session tool. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class CurrentToolMetadata +{ + /// 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; + + /// Optional MCP/config namespaced tool name. + [JsonPropertyName("namespacedName")] + public string? NamespacedName { get; set; } +} + +/// Current lightweight tool metadata snapshot for the session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ToolsGetCurrentMetadataResult +{ + /// Current tool metadata, or null when tools have not been initialized yet. + [JsonPropertyName("tools")] + public IList? Tools { get; set; } +} + +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionToolsGetCurrentMetadataRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Empty result after replacing the calling connection's externally implemented tools. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ToolsSetResult +{ +} + +/// Serializable definition of a caller-implemented tool whose execution is handled over the SDK connection. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ProtocolExternalToolDefinition +{ + /// Tool-loading deferral policy. + [JsonPropertyName("defer")] + public ProtocolExternalToolDefer? Defer { get; set; } + + /// Model-visible explanation of what the tool does. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// Whether the tool executes commands in a terminal. + [JsonPropertyName("isTerminal")] + public bool? IsTerminal { 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; } +} + +/// 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)] +internal sealed class ToolsSetRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Complete replacement list for the calling connection. + [JsonPropertyName("tools")] + public IList Tools { get => field ??= []; set; } +} + +/// Empty result after applying subagent settings. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ToolsUpdateSubagentSettingsResult +{ +} + +/// Subagent model, reasoning effort, context tier, and auto-invocation settings. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SubagentSettingsEntry +{ + /// 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; } + + /// 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; + + /// Subagent settings to apply, or null to clear the live session override. + [JsonPropertyName("subagents")] + public UpdateSubagentSettingsRequestSubagents? Subagents { get; set; } +} + +/// RPC data type for SessionCommandsList operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionCommandsListRequest +{ + /// 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; } +} + +/// RPC data type for SessionCommandsListRequestWithSession operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionCommandsListRequestWithSession +{ + /// 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; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// 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; +} + + +/// 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 SlashCommandInvocationResultText : SlashCommandInvocationResult +{ + /// + [JsonIgnore] + public override string Kind => "text"; + + /// Whether text contains Markdown. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("markdown")] + public bool? Markdown { get; set; } + + /// 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; } +} + +/// 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 SlashCommandInvocationResultAgentPrompt : SlashCommandInvocationResult +{ + /// + [JsonIgnore] + 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; } +} + +/// 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 SlashCommandInvocationResultCompleted : SlashCommandInvocationResult +{ + /// + [JsonIgnore] + public override string Kind => "completed"; + + /// Optional user-facing message describing the completed command. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { 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; } +} + +/// Selectable slash-command subcommand option with name, description, and optional group label. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SlashCommandSelectSubcommandOption +{ + /// Human-readable description of the subcommand. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// 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; +} + +/// 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 SlashCommandInvocationResultSelectSubcommand : SlashCommandInvocationResult +{ + /// + [JsonIgnore] + 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; } +} + +/// RPC data type for SlashCommandTimelineEntry operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SlashCommandTimelineEntry +{ + /// 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; } + + /// 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; } +} + +/// The add-timeline-entry variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SlashCommandInvocationResultAddTimelineEntry : SlashCommandInvocationResult +{ + /// + [JsonIgnore] + public override string Kind => "add-timeline-entry"; + + /// 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("prefillInput")] + public string? PrefillInput { get; set; } + + /// Whether command execution changed persisted runtime settings. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("runtimeSettingsChanged")] + public bool? RuntimeSettingsChanged { get; set; } +} + +/// RPC data type for SlashCommandModelPickerDialog operations. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SlashCommandModelPickerDialog +{ + /// 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; } + + /// Model-selection target represented by the picker. + [JsonPropertyName("target")] + public string? Target { get; set; } +} + +/// The show-dialog variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SlashCommandInvocationResultShowDialog : SlashCommandInvocationResult +{ + /// + [JsonIgnore] + public override string Kind => "show-dialog"; + + /// Dialog the host should display. + [JsonPropertyName("dialog")] + public required SlashCommandModelPickerDialog Dialog { get; set; } + + /// Whether command execution changed persisted runtime settings. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("runtimeSettingsChanged")] + public bool? RuntimeSettingsChanged { get; set; } +} + +/// 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 SlashCommandInvocationResultSetModel : SlashCommandInvocationResult +{ + /// + [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; } + + /// 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; } +} + +/// The set-plan-model variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class SlashCommandInvocationResultSetPlanModel : SlashCommandInvocationResult +{ + /// + [JsonIgnore] + public override string Kind => "set-plan-model"; + + /// 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; } +} + +/// Slash command name and optional raw input string to invoke. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class CommandsInvokeRequest +{ + /// 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; + + /// Optional client surface that initiated the invocation. + [JsonPropertyName("origin")] + public CommandsInvocationOrigin? Origin { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Whether finalizing the invocation effect succeeded, and the failure reason when it did not. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class CommandsFinalizeInvocationEffectResult +{ + /// 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; } +} + +/// The slash-command result object that produced the pending effect, echoed back unchanged. +public sealed class CommandsFinalizeInvocationEffectRequestEffect +{ +} + +/// The pending slash-command invocation effect to finalize, plus whether the host applied or cancelled it. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class CommandsFinalizeInvocationEffectRequest +{ + /// The slash-command result object that produced the pending effect, echoed back unchanged. + [JsonPropertyName("effect")] + public CommandsFinalizeInvocationEffectRequestEffect Effect { get => field ??= new(); set; } + + /// Whether the host applied or cancelled the pending invocation effect. + [JsonPropertyName("outcome")] + public CommandsInvocationEffectOutcome Outcome { get; set; } + + /// 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; } +} + +/// Pending command request ID and an optional error if the client handler failed. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class CommandsHandlePendingCommandRequest +{ + /// 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; +} + +/// Error message produced while executing the command, if any. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ExecuteCommandResult +{ + /// Error message produced while executing the command, if any. Omitted when the handler succeeded. + [JsonPropertyName("error")] + public string? Error { get; set; } +} + +/// Slash command name and argument string to execute synchronously. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class ExecuteCommandParams +{ + /// Argument string to pass to the command (empty string if none). + [JsonPropertyName("args")] + public string Args { get; set; } = string.Empty; + + /// 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; +} + +/// 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; } + + /// Stable opaque ID of the queued command. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("queueId")] + public string? QueueId { 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; + + /// 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; +} + +/// 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; } +} + +/// 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; } + + /// 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; } +} + +/// 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)] +internal sealed class CommandsRespondToQueuedCommandRequest +{ + /// Request ID from the `command.queued` event the host is responding to. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + /// Result of the queued command execution. + [JsonPropertyName("result")] + public QueuedCommandResult Result { get => field ??= new(); set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { 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; } +} + +/// 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; +} + +/// 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; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { 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. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class UIEphemeralQueryResult +{ + /// Answer returned by the model. + [JsonPropertyName("answer")] + public string Answer { get; set; } = string.Empty; +} + +/// 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; + + /// 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; } + + /// The user's response: accept (submitted), decline (rejected), or cancel (dismissed). + [JsonPropertyName("action")] + public UIElicitationResponseAction Action { get; set; } + + /// The form values submitted by the user (present when action is 'accept'). + [JsonPropertyName("content")] + public IDictionary? Content { get; set; } +} + +/// JSON Schema describing the form fields to present to the user. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class UIElicitationSchema +{ + /// Form field definitions, keyed by field name. + [JsonPropertyName("properties")] + public IDictionary Properties { get => field ??= new Dictionary(); 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; +} + +/// 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; } +} + +/// Prompt message and JSON schema describing the form fields to elicit from the user. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class UIElicitationRequest +{ + /// 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; + + /// Elicitation mode. Omitted and form are equivalent for structured elicitation. + [JsonPropertyName("mode")] + public McpElicitationFormMode? Mode { get; set; } + + /// JSON Schema describing the form fields to present to the user. + [JsonPropertyName("requestedSchema")] + public UIElicitationSchema RequestedSchema { get => field ??= new(); set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// MCP task metadata. + [JsonPropertyName("task")] + public McpTaskMetadata? Task { get; set; } +} + +/// Indicates whether the elicitation response was accepted; false if it was already resolved by another client. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class UIElicitationResult +{ + /// Whether the response was accepted. False if the request was already resolved by another client. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// Pending elicitation request ID and the user's response (accept/decline/cancel + form values). +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class UIHandlePendingElicitationRequest +{ + /// 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; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Indicates whether the pending UI request was resolved by this call. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class UIHandlePendingResult +{ + /// 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; } +} + +/// 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 UIUserInputResponse +{ + /// 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; } +} + +/// Request ID of a pending `user_input.requested` event and the user's response. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +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; +} + +/// Optional sampling result payload. Omit to reject/cancel the sampling request without providing a result. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class UIHandlePendingSamplingResponse +{ +} + +/// 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 UIHandlePendingSamplingRequest +{ + /// 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; +} + +/// Request ID of a pending `auto_mode_switch.requested` event and the user's response. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class UIHandlePendingAutoModeSwitchRequest +{ + /// The unique request ID from the auto_mode_switch.requested event. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + /// 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; +} + +/// The user's selected action for an exhausted session limit. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class UISessionLimitsExhaustedResponse +{ + /// 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; } +} + +/// 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 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; +} + +/// 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 UIExitPlanModeResponse +{ + /// 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; } - /// Current status: running, disabled, failed, or starting. - [JsonPropertyName("status")] - public ExtensionStatus Status { get; set; } + /// The action the user selected. Defaults to 'autopilot' when autoApproveEdits is true, otherwise 'interactive'. + [JsonPropertyName("selectedAction")] + public UIExitPlanModeAction? SelectedAction { get; set; } } -/// Extensions discovered for the session, with their current status. +/// Request ID of a pending `exit_plan_mode.requested` event and the user's response. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ExtensionList +internal sealed class UIHandlePendingExitPlanModeRequest { - /// Discovered extensions and their current status. - [JsonPropertyName("extensions")] - public IList Extensions { get => field ??= []; set; } -} + /// 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; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionExtensionsListRequest -{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Source-qualified extension identifier to enable for the session. +/// 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)] -internal sealed class ExtensionsEnableRequest +public sealed class UIRegisterDirectAutoModeSwitchHandlerResult { - /// Source-qualified extension ID to enable. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// 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; +} +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionUiRegisterDirectAutoModeSwitchHandlerRequest +{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Source-qualified extension identifier to disable for the session. +/// Indicates whether the handle was active and the registration count was decremented. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ExtensionsDisableRequest +public sealed class UIUnregisterDirectAutoModeSwitchHandlerResult { - /// 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; + /// True if the handle was active and decremented the counter; false if the handle was unknown. + [JsonPropertyName("unregistered")] + public bool Unregistered { get; set; } } -/// Identifies the target session. +/// Opaque handle previously returned by `registerDirectAutoModeSwitchHandler` to release. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionExtensionsReloadRequest +internal sealed class UIUnregisterDirectAutoModeSwitchHandlerRequest { + /// 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; } -/// Attachment union accepted by push input, covering files, directories, GitHub objects, blobs, snippets, and extension context. -/// Polymorphic base type discriminated by type. +/// Indicates whether the operation succeeded. [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 +public sealed class PermissionsConfigureResult { - /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } - -/// Optional line range to scope the attachment to a specific section of the file. +/// Source descriptor for a `session.permissions.configure` content-exclusion rule, with source name and type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PushAttachmentFileLineRange +public sealed class PermissionsConfigureAdditionalContentExclusionPolicyRuleSource { - /// End line number (1-based, inclusive). - [JsonPropertyName("end")] - public long End { get; set; } + /// Name of the policy source. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Start line number (1-based). - [JsonPropertyName("start")] - public long Start { get; set; } + /// Type of the policy source. + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; } -/// File attachment. -/// The file variant of . +/// Single content-exclusion rule supplied to `session.permissions.configure`, with paths, match conditions, and source. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentFile : PushAttachment +public sealed class PermissionsConfigureAdditionalContentExclusionPolicyRule { - /// - [JsonIgnore] - public override string Type => "file"; + /// Conditions of which at least one must match. + [JsonPropertyName("ifAnyMatch")] + public IList? IfAnyMatch { get; set; } - /// User-facing display name for the attachment. - [JsonPropertyName("displayName")] - public required string DisplayName { get; set; } + /// Conditions none of which may match. + [JsonPropertyName("ifNoneMatch")] + public IList? IfNoneMatch { 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; } + /// Path patterns covered by this rule. + [JsonPropertyName("paths")] + public IList Paths { get => field ??= []; set; } - /// Absolute file path. - [JsonPropertyName("path")] - public required string Path { get; 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; } } -/// Directory attachment. -/// The directory variant of . +/// Content-exclusion policy supplied to `session.permissions.configure`, with rules, last-updated data, and scope. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentDirectory : PushAttachment +public sealed class PermissionsConfigureAdditionalContentExclusionPolicy { - /// - [JsonIgnore] - public override string Type => "directory"; + /// Opaque policy update timestamp supplied by the host. + [JsonPropertyName("last_updated_at")] + public JsonElement LastUpdatedAt { get; set; } - /// User-facing display name for the attachment. - [JsonPropertyName("displayName")] - public required string DisplayName { get; set; } + /// Content-exclusion rules to apply. + [JsonPropertyName("rules")] + public IList Rules { get => field ??= []; set; } - /// Absolute directory path. - [JsonPropertyName("path")] - public required string Path { get; set; } + /// Allowed values for the `PermissionsConfigureAdditionalContentExclusionPolicyScope` enumeration. + [JsonPropertyName("scope")] + public PermissionsConfigureAdditionalContentExclusionPolicyScope Scope { get; set; } } -/// End position of the selection. +/// 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 PushAttachmentSelectionDetailsEnd +public sealed class PermissionPathsConfig { - /// End character offset within the line (0-based). - [JsonPropertyName("character")] - public long Character { 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; } - /// End line number (0-based). - [JsonPropertyName("line")] - public long Line { 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; } -/// 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; } + /// If true, the runtime allows access to all paths without prompting. Equivalent to constructing an UnrestrictedPathManager. + [JsonPropertyName("unrestricted")] + public bool? Unrestricted { get; set; } - /// Start line number (0-based). - [JsonPropertyName("line")] - public long Line { 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; } } -/// Position range of the selection within the file. +/// If specified, replaces the session's approved/denied permission rules. Omit to leave the current rules unchanged. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PushAttachmentSelectionDetails +public sealed class PermissionRulesSet { - /// End position of the selection. - [JsonPropertyName("end")] - public PushAttachmentSelectionDetailsEnd End { get => field ??= new(); set; } + /// Rules that auto-approve matching requests. + [JsonPropertyName("approved")] + public IList Approved { get => field ??= []; set; } - /// Start position of the selection. - [JsonPropertyName("start")] - public PushAttachmentSelectionDetailsStart Start { get => field ??= new(); set; } + /// Rules that auto-deny matching requests. + [JsonPropertyName("denied")] + public IList Denied { get => field ??= []; set; } } -/// Code selection attachment from an editor. -/// The selection variant of . +/// 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 partial class PushAttachmentSelection : PushAttachment +public sealed class PermissionUrlsConfig { - /// - [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; } - - /// Position range of the selection within the file. - [JsonPropertyName("selection")] - public required PushAttachmentSelectionDetails Selection { 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; } - /// The selected text content. - [JsonPropertyName("text")] - public required string Text { 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; } } -/// GitHub issue, pull request, or discussion reference. -/// The github_reference variant of . +/// Patch of permission policy fields to apply (omit a field to leave it unchanged). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubReference : PushAttachment +internal sealed class PermissionsConfigureParams { - /// - [JsonIgnore] - public override string Type => "github_reference"; + /// 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; } - /// Issue, pull request, or discussion number. - [JsonPropertyName("number")] - public required long Number { 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; } - /// Type of GitHub reference. - [JsonPropertyName("referenceType")] - public required PushAttachmentGitHubReferenceType ReferenceType { 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; } - /// Current state of the referenced item (e.g., open, closed, merged). - [JsonPropertyName("state")] - public required string State { 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; } - /// Title of the referenced item. - [JsonPropertyName("title")] - public required string Title { 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; } - /// 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; + + /// 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; } } -/// Pointer to a GitHub repository. +/// Indicates whether the permission decision was applied; false when the request was already resolved. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PushGitHubRepoRef +public sealed class PermissionRequestResult { - /// Numeric GitHub repository id. - [JsonPropertyName("id")] - public long? Id { get; set; } - - /// Repository name (without owner). - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Whether the permission request was handled successfully. + [JsonPropertyName("success")] + public bool Success { get; set; } +} - /// Repository owner login (user or organization). - [JsonPropertyName("owner")] - public string Owner { get; set; } = string.Empty; +/// The client's response to the pending permission prompt. +/// Polymorphic base type discriminated by kind. +[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 +{ + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Pointer to a GitHub commit. -/// The github_commit variant of . + +/// 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 PushAttachmentGitHubCommit : PushAttachment +public partial class PermissionDecisionApproveOnce : PermissionDecision { /// [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; } + public override string Kind => "approve-once"; - /// Repository the commit belongs to. - [JsonPropertyName("repo")] - public required PushGitHubRepoRef Repo { 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; } +} - /// URL to the commit on GitHub. - [JsonPropertyName("url")] - public required string Url { get; set; } +/// 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(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")] + public virtual string Kind { get; set; } = string.Empty; } -/// Pointer to a GitHub release. -/// The github_release variant of . + +/// Session-scoped approval details for specific command identifiers. +/// The commands variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubRelease : PushAttachment +public partial class PermissionDecisionApproveForSessionApprovalCommands : PermissionDecisionApproveForSessionApproval { /// [JsonIgnore] - public override string Type => "github_release"; - - /// Human-readable release name. - [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 override string Kind => "commands"; - /// URL to the release on GitHub. - [JsonPropertyName("url")] - public required string Url { get; set; } + /// Command identifiers covered by this approval. + [JsonPropertyName("commandIdentifiers")] + public required IList CommandIdentifiers { get; set; } } -/// Pointer to a GitHub Actions job. -/// The github_actions_job variant of . +/// Session-scoped approval details for read-only filesystem operations. +/// The read variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubActionsJob : PushAttachment +public partial class PermissionDecisionApproveForSessionApprovalRead : PermissionDecisionApproveForSessionApproval { /// [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; } - - /// 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; } + public override string Kind => "read"; } -/// Pointer to a GitHub repository. -/// The github_repository variant of . +/// Session-scoped approval details for filesystem write operations. +/// The write variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubRepository : PushAttachment +public partial class PermissionDecisionApproveForSessionApprovalWrite : PermissionDecisionApproveForSessionApproval { /// [JsonIgnore] - public override string Type => "github_repository"; - - /// Short description of the repository. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("description")] - public string? Description { get; set; } - - /// 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; } - - /// Repository pointer. - [JsonPropertyName("repo")] - public required PushGitHubRepoRef Repo { get; set; } - - /// URL to the repository on GitHub. - [JsonPropertyName("url")] - public required string Url { get; set; } + public override string Kind => "write"; } -/// One side of a file diff (head or base). +/// 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 sealed class PushAttachmentGitHubFileDiffSide +public partial class PermissionDecisionApproveForSessionApprovalMcp : PermissionDecisionApproveForSessionApproval { - /// Repository-relative path to the file. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "mcp"; - /// Git ref (branch, tag, or commit SHA) the file is read at. - [JsonPropertyName("ref")] - public string Ref { get; set; } = string.Empty; + /// MCP server name. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } - /// Repository the file lives in. - [JsonPropertyName("repo")] - public PushGitHubRepoRef Repo { get => field ??= new(); set; } + /// MCP tool name, or null to cover every tool on the server. + [JsonPropertyName("toolName")] + public string? ToolName { get; set; } } -/// Pointer to a single-file diff. At least one of `head` and `base` must be present. -/// The github_file_diff 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 PushAttachmentGitHubFileDiff : PushAttachment +public partial class PermissionDecisionApproveForSessionApprovalMcpSampling : PermissionDecisionApproveForSessionApproval { /// [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; } + public override string Kind => "mcp-sampling"; - /// URL to the diff on GitHub (e.g., a commit, compare, or PR-file URL). - [JsonPropertyName("url")] - public required string Url { get; set; } + /// MCP server name. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } } -/// One side of a tree comparison (head or base). +/// Session-scoped approval details for writes to long-term memory. +/// The memory variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PushAttachmentGitHubTreeComparisonSide +public partial class PermissionDecisionApproveForSessionApprovalMemory : PermissionDecisionApproveForSessionApproval { - /// 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; + /// + [JsonIgnore] + public override string Kind => "memory"; } -/// Pointer to a comparison between two git revisions. -/// The github_tree_comparison 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 PushAttachmentGitHubTreeComparison : PushAttachment +public partial class PermissionDecisionApproveForSessionApprovalCustomTool : PermissionDecisionApproveForSessionApproval { /// [JsonIgnore] - public override string Type => "github_tree_comparison"; - - /// Base side of the comparison. - [JsonPropertyName("base")] - public required PushAttachmentGitHubTreeComparisonSide Base { get; set; } - - /// Head side of the comparison. - [JsonPropertyName("head")] - public required PushAttachmentGitHubTreeComparisonSide Head { get; set; } + public override string Kind => "custom-tool"; - /// URL to the comparison on GitHub. - [JsonPropertyName("url")] - public required string Url { get; set; } + /// Custom tool name. + [JsonPropertyName("toolName")] + public required string ToolName { get; set; } } -/// Generic GitHub URL reference. -/// The github_url 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 PushAttachmentGitHubUrl : PushAttachment +public partial class PermissionDecisionApproveForSessionApprovalExtensionManagement : PermissionDecisionApproveForSessionApproval { /// [JsonIgnore] - public override string Type => "github_url"; + public override string Kind => "extension-management"; - /// URL to the GitHub resource. - [JsonPropertyName("url")] - public required string Url { get; set; } + /// Optional operation identifier; when omitted, the approval covers all extension management operations. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("operation")] + public string? Operation { get; set; } } -/// Pointer to a file in a GitHub repository at a specific ref. -/// The github_file variant of . +/// Session-scoped workflow approval, optionally narrowed by approval key. +/// The workflow variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PushAttachmentGitHubFile : PushAttachment +public partial class PermissionDecisionApproveForSessionApprovalWorkflow : PermissionDecisionApproveForSessionApproval { /// [JsonIgnore] - public override string Type => "github_file"; - - /// 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; } + public override string Kind => "workflow"; - /// URL to the file on GitHub. - [JsonPropertyName("url")] - public required string Url { 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; } } -/// Pointer to a line range inside a file in a GitHub repository. -/// The github_snippet 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 PushAttachmentGitHubSnippet : PushAttachment +public partial class PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess : PermissionDecisionApproveForSessionApproval { /// [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; } - - /// Repository the file lives in. - [JsonPropertyName("repo")] - public required PushGitHubRepoRef Repo { get; set; } + public override string Kind => "extension-permission-access"; - /// URL to the snippet on GitHub (with line anchor). - [JsonPropertyName("url")] - public required string Url { get; set; } + /// Extension name. + [JsonPropertyName("extensionName")] + public required string ExtensionName { get; set; } } -/// Blob attachment with inline base64-encoded data. -/// The blob 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 PushAttachmentBlob : PushAttachment +public partial class PermissionDecisionApproveForSessionApprovalExtensionEnvAccess : PermissionDecisionApproveForSessionApproval { /// [JsonIgnore] - public override string Type => "blob"; - - /// Base64-encoded content. - [Base64String] - [JsonPropertyName("data")] - public required string Data { get; set; } + public override string Kind => "extension-env-access"; - /// User-facing display name for the attachment. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } + /// Names of the sensitive environment variables this approval covers. Values are never persisted. + [JsonPropertyName("environmentVariables")] + public required IList EnvironmentVariables { get; set; } - /// MIME type of the inline data. - [JsonPropertyName("mimeType")] - public required string MimeType { get; set; } + /// Extension name. + [JsonPropertyName("extensionName")] + public required string ExtensionName { get; set; } } -/// Slim input shape for extension_context attachments; identity fields are runtime-derived. -/// The extension_context variant of . +/// 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)] -public partial class PushAttachmentExtensionContext : PushAttachment +public partial class PermissionDecisionApproveForSession : PermissionDecision { /// [JsonIgnore] - public override string Type => "extension_context"; + public override string Kind => "approve-for-session"; - /// Caller-supplied JSON payload (required, may be null but not undefined). - [JsonPropertyName("payload")] - public required JsonElement Payload { get; set; } + /// Session-scoped approval to remember (tool prompts only; omitted for path/url prompts). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("approval")] + public PermissionDecisionApproveForSessionApproval? Approval { 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; } + /// URL domain to approve for the rest of the session (URL prompts only). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("domain")] + public string? Domain { get; set; } } -/// Parameters for session.extensions.sendAttachmentsToMessage. +/// 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)] -internal sealed class SendAttachmentsToMessageParams +public partial class PermissionDecisionApproveReadOnlyForSession : PermissionDecision { - /// 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; } + /// + [JsonIgnore] + public override string Kind => "approve-read-only-for-session"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Canonical directories covered by the read-only grant. + [JsonPropertyName("directories")] + public required IList Directories { get; set; } } -/// A tool name and arguments to execute through the session's native invocation pipeline. +/// Approval to persist for this location. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ToolsExecuteRequest +[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 { - /// Arguments supplied to the tool. - [JsonPropertyName("arguments")] - public JsonElement Arguments { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} - /// 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; +/// 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 identifier used to correlate this invocation with its tool call. - [JsonPropertyName("toolCallId")] - public string? ToolCallId { get; set; } + /// Command identifiers covered by this approval. + [JsonPropertyName("commandIdentifiers")] + public required IList CommandIdentifiers { get; set; } } -/// Custom grammar input format accepted by a built-in tool. +/// Location-scoped approval details for read-only filesystem operations. +/// The read variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class BuiltinToolFormat +public partial class PermissionDecisionApproveForLocationApprovalRead : PermissionDecisionApproveForLocationApproval { - /// Grammar definition accepted by the tool. - [JsonPropertyName("definition")] - public string Definition { get; set; } = string.Empty; - - /// Grammar syntax used by the format definition. - [JsonPropertyName("syntax")] - public string Syntax { get; set; } = string.Empty; - - /// Custom input-format discriminator. - [JsonPropertyName("type")] - public BuiltinToolFormatType Type { get; set; } + /// + [JsonIgnore] + public override string Kind => "read"; } -/// JSON Schema object accepted by a built-in tool. +/// Location-scoped approval details for filesystem write operations. +/// The write variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class BuiltinToolInputSchema +public partial class PermissionDecisionApproveForLocationApprovalWrite : PermissionDecisionApproveForLocationApproval { - /// Root type of the tool input schema. - [JsonPropertyName("type")] - public BuiltinToolInputSchemaType Type { get; set; } + /// + [JsonIgnore] + public override string Kind => "write"; } -/// Rust-owned metadata and input schema for a built-in tool. +/// 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 BuiltinToolDescriptor +public partial class PermissionDecisionApproveForLocationApprovalMcp : PermissionDecisionApproveForLocationApproval { - /// 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; } - - /// Whether the tool provides a specialized intention summary. - [JsonPropertyName("hasSummariseIntention")] - public bool HasSummariseIntention { get; set; } - - /// JSON Schema for the tool input, or null when the tool uses a custom format. - [JsonPropertyName("inputSchema")] - public BuiltinToolInputSchema? InputSchema { get; set; } - - /// Optional supplemental usage instructions for the tool. - [JsonPropertyName("instructions")] - public string? Instructions { get; set; } - - /// Whether the tool executes commands in a terminal. - [JsonPropertyName("isTerminal")] - public bool IsTerminal { get; set; } + /// + [JsonIgnore] + public override string Kind => "mcp"; - /// Stable name used to invoke the built-in tool. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// MCP server name. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } - /// Policy describing which tool metadata may be recorded without obfuscation. - [JsonPropertyName("safeForTelemetry")] - public JsonElement SafeForTelemetry { get; set; } + /// MCP tool name, or null to cover every tool on the server. + [JsonPropertyName("toolName")] + public string? ToolName { get; set; } +} - /// Optional human-readable title for the tool. - [JsonPropertyName("title")] - public string? Title { get; set; } +/// Location-scoped approval details for MCP sampling requests from a server. +/// The mcp-sampling variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionDecisionApproveForLocationApprovalMcpSampling : PermissionDecisionApproveForLocationApproval +{ + /// + [JsonIgnore] + public override string Kind => "mcp-sampling"; - /// Optional tool category discriminator. - [JsonPropertyName("type")] - public string? Type { get; set; } + /// MCP server name. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } } -/// Rust-owned built-in tool descriptors for the session. +/// Location-scoped approval details for writes to long-term memory. +/// The memory variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolsGetBuiltinDescriptorsResult +public partial class PermissionDecisionApproveForLocationApprovalMemory : PermissionDecisionApproveForLocationApproval { - /// Built-in tool descriptors materialized for the session. - [JsonPropertyName("tools")] - public IList Tools { get => field ??= []; set; } + /// + [JsonIgnore] + public override string Kind => "memory"; } -/// Shell-specific names and description lines used to materialize built-in shell tool descriptors. +/// Location-scoped approval details for a custom tool, keyed by tool name. +/// The custom-tool variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolsShellDescriptorConfig +public partial class PermissionDecisionApproveForLocationApprovalCustomTool : PermissionDecisionApproveForLocationApproval { - /// Additional model-facing shell description lines. - [JsonPropertyName("descriptionLines")] - public IList DescriptionLines { get => field ??= []; set; } - - /// Human-readable shell name. - [JsonPropertyName("displayName")] - public string DisplayName { get; set; } = string.Empty; - - /// Tool name used to list active shells. - [JsonPropertyName("listShellsToolName")] - public string ListShellsToolName { get; set; } = string.Empty; - - /// Tool name used to read shell output. - [JsonPropertyName("readShellToolName")] - public string ReadShellToolName { get; set; } = string.Empty; - - /// Tool name used to start shell commands. - [JsonPropertyName("shellToolName")] - public string ShellToolName { get; set; } = string.Empty; - - /// Stable shell type identifier. - [JsonPropertyName("shellType")] - public string ShellType { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "custom-tool"; - /// Tool name used to stop shell commands. - [JsonPropertyName("stopShellToolName")] - public string StopShellToolName { get; set; } = string.Empty; + /// Custom tool name. + [JsonPropertyName("toolName")] + public required string ToolName { get; set; } } -/// Options controlling how Rust-owned built-in tool descriptors are materialized. +/// Location-scoped approval details for extension-management operations, optionally narrowed by operation. +/// The extension-management variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ToolsGetBuiltinDescriptorsRequest +public partial class PermissionDecisionApproveForLocationApprovalExtensionManagement : PermissionDecisionApproveForLocationApproval { - /// Whether background task completion notifications are enabled. - [JsonPropertyName("backgroundTaskNotificationsEnabled")] - public bool? BackgroundTaskNotificationsEnabled { get; set; } - - /// 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; - - /// Shell-specific names and description lines for shell tools. - [JsonPropertyName("shellConfig")] - public ToolsShellDescriptorConfig? ShellConfig { get; set; } - - /// Whether the configured shell supports PowerShell 7 syntax. - [JsonPropertyName("shellSupportsPowerShell7Syntax")] - public bool? ShellSupportsPowerShell7Syntax { get; set; } - - /// Default shell timeout in milliseconds. - [JsonPropertyName("shellTimeoutMs")] - public double? ShellTimeoutMs { get; set; } + /// + [JsonIgnore] + public override string Kind => "extension-management"; - /// Whether semantic skill lookup is available. - [JsonPropertyName("skillEmbeddingEnabled")] - public bool? SkillEmbeddingEnabled { get; set; } + /// Optional operation identifier; when omitted, the approval covers all extension management operations. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("operation")] + public string? Operation { get; set; } } -/// Task completion notification with summary from the agent. +/// Location-scoped workflow approval, optionally narrowed by approval key. +/// The workflow variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class TaskCompleteData +public partial class PermissionDecisionApproveForLocationApprovalWorkflow : PermissionDecisionApproveForLocationApproval { - /// 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; } + /// + [JsonIgnore] + public override string Kind => "workflow"; - /// Summary of the completed task, provided by the agent. - [JsonPropertyName("summary")] - public string? Summary { 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; } } -/// Binary result returned by a tool for the model. +/// 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 sealed class ExternalToolTextResultForLlmBinaryResultsForLlm +public partial class PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess : PermissionDecisionApproveForLocationApproval { - /// Base64-encoded binary data. - [Base64String] - [JsonPropertyName("data")] - public string Data { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "extension-permission-access"; - /// Human-readable description of the binary data. - [JsonPropertyName("description")] - public string? Description { get; set; } + /// Extension name. + [JsonPropertyName("extensionName")] + public required string ExtensionName { get; set; } +} - /// Optional metadata from the producing tool. - [JsonPropertyName("metadata")] - public IDictionary? Metadata { 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"; - /// MIME type of the binary data. - [JsonPropertyName("mimeType")] - public string MimeType { get; set; } = string.Empty; + /// Names of the sensitive environment variables this approval covers. Values are never persisted. + [JsonPropertyName("environmentVariables")] + public required IList EnvironmentVariables { get; set; } - /// Binary result type discriminator. Use "image" for images and "resource" for other binary data. - [JsonPropertyName("type")] - public ExternalToolTextResultForLlmBinaryResultsForLlmType Type { get; set; } + /// Extension name. + [JsonPropertyName("extensionName")] + public required string ExtensionName { 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. +/// 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)] -[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 partial class PermissionDecisionApproveForLocation : PermissionDecision { - /// The type discriminator. - [JsonPropertyName("type")] - public virtual string Type { get; set; } = string.Empty; -} + /// + [JsonIgnore] + public override string Kind => "approve-for-location"; + + /// Approval to persist for this location. + [JsonPropertyName("approval")] + public required PermissionDecisionApproveForLocationApproval Approval { get; set; } + /// Location key (git root or cwd) to persist the approval to. + [JsonPropertyName("locationKey")] + public required string LocationKey { get; set; } +} -/// Plain text content block. -/// The text variant of . +/// 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 ExternalToolTextResultForLlmContentText : ExternalToolTextResultForLlmContent +public partial class PermissionDecisionApprovePermanently : PermissionDecision { /// [JsonIgnore] - public override string Type => "text"; + public override string Kind => "approve-permanently"; - /// The text content. - [JsonPropertyName("text")] - public required string Text { get; set; } + /// URL domain to approve permanently. + [JsonPropertyName("domain")] + public required string Domain { get; set; } } -/// Terminal/shell output content block with optional exit code and working directory. -/// The terminal variant of . +/// Permission-decision request variant to reject a pending permission request, with optional feedback. +/// The reject variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ExternalToolTextResultForLlmContentTerminal : ExternalToolTextResultForLlmContent +public partial class PermissionDecisionReject : PermissionDecision { /// [JsonIgnore] - public override string Type => "terminal"; - - /// Working directory where the command was executed. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("cwd")] - public string? Cwd { get; set; } + public override string Kind => "reject"; - /// Process exit code, if the command has completed. + /// Optional feedback explaining the rejection. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("exitCode")] - public long? ExitCode { get; set; } - - /// Terminal/shell output text. - [JsonPropertyName("text")] - public required string Text { get; set; } + [JsonPropertyName("feedback")] + public string? Feedback { get; set; } } -/// Shell command exit metadata with optional output preview. -/// The shell_exit variant of . +/// 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 ExternalToolTextResultForLlmContentShellExit : ExternalToolTextResultForLlmContent +public partial class PermissionDecisionUserNotAvailable : PermissionDecision { /// [JsonIgnore] - public override string Type => "shell_exit"; - - /// 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; } + public override string Kind => "user-not-available"; } -/// Image content block with base64-encoded data. -/// The image variant of . +/// Permission-decision variant indicating the request was approved. +/// The approved variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class ExternalToolTextResultForLlmContentImage : ExternalToolTextResultForLlmContent +public partial class PermissionDecisionApproved : PermissionDecision { /// [JsonIgnore] - public override string Type => "image"; - - /// Base64-encoded image data. - [Base64String] - [JsonPropertyName("data")] - public required string Data { get; set; } - - /// MIME type of the image (e.g., image/png, image/jpeg). - [JsonPropertyName("mimeType")] - public required string MimeType { get; set; } + public override string Kind => "approved"; } -/// Audio content block with base64-encoded data. -/// The audio variant of . +/// 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 ExternalToolTextResultForLlmContentAudio : ExternalToolTextResultForLlmContent +public partial class PermissionDecisionApprovedForSession : PermissionDecision { /// [JsonIgnore] - public override string Type => "audio"; - - /// Base64-encoded audio data. - [Base64String] - [JsonPropertyName("data")] - public required string Data { get; set; } + public override string Kind => "approved-for-session"; - /// MIME type of the audio (e.g., audio/wav, audio/mpeg). - [JsonPropertyName("mimeType")] - public required string MimeType { get; set; } + /// The approval to add as a session-scoped rule. + [JsonPropertyName("approval")] + public required UserToolSessionApproval Approval { get; set; } } -/// Icon image for a resource. +/// 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 sealed class ExternalToolTextResultForLlmContentResourceLinkIcon +public partial class PermissionDecisionApprovedForLocation : PermissionDecision { - /// MIME type of the icon image. - [JsonPropertyName("mimeType")] - public string? MimeType { get; set; } - - /// Available icon sizes (e.g., ['16x16', '32x32']). - [JsonPropertyName("sizes")] - public IList? Sizes { get; set; } + /// + [JsonIgnore] + public override string Kind => "approved-for-location"; - /// URL or path to the icon image. - [JsonPropertyName("src")] - public string Src { get; set; } = string.Empty; + /// The approval to persist for this location. + [JsonPropertyName("approval")] + public required UserToolSessionApproval Approval { get; set; } - /// Theme variant this icon is intended for. - [JsonPropertyName("theme")] - public ExternalToolTextResultForLlmContentResourceLinkIconTheme? Theme { get; set; } + /// The location key (git root or cwd) to persist the approval to. + [JsonPropertyName("locationKey")] + public required string LocationKey { get; set; } } -/// Resource link content block referencing an external resource. -/// The resource_link variant of . +/// 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 ExternalToolTextResultForLlmContentResourceLink : ExternalToolTextResultForLlmContent +public partial class PermissionDecisionCancelled : PermissionDecision { /// [JsonIgnore] - public override string Type => "resource_link"; - - /// Human-readable description of the resource. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("description")] - public string? Description { get; set; } - - /// 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; } + public override string Kind => "cancelled"; - /// Human-readable display title for the resource. + /// Optional explanation of why the request was cancelled. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("title")] - public string? Title { get; set; } - - /// URI identifying the resource. - [JsonPropertyName("uri")] - public required string Uri { get; set; } + [JsonPropertyName("reason")] + public string? Reason { get; set; } } -/// Embedded resource content block with inline text or binary data. -/// The resource variant of . +/// 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 ExternalToolTextResultForLlmContentResource : ExternalToolTextResultForLlmContent +public partial class PermissionDecisionDeniedByRules : PermissionDecision { /// [JsonIgnore] - public override string Type => "resource"; + public override string Kind => "denied-by-rules"; - /// The embedded resource contents, either text or base64-encoded binary. - [JsonPropertyName("resource")] - public required JsonElement Resource { get; set; } + /// Rules that denied the request. + [JsonPropertyName("rules")] + public required IList Rules { get; set; } } -/// A message injected by a tool result. +/// 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 sealed class ToolResultNewMessage +public partial class PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser : PermissionDecision { - /// 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; + /// + [JsonIgnore] + public override string Kind => "denied-no-approval-rule-and-could-not-request-from-user"; } -/// RPC data type for TaskCompletionDecision operations. +/// 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)] -public sealed class TaskCompletionDecision +public partial class PermissionDecisionDeniedInteractivelyByUser : PermissionDecision { - /// Objective eligibility token captured when the decision was evaluated. - [JsonPropertyName("completionEligibilityToken")] - public long? CompletionEligibilityToken { get; set; } - - /// Whether completion was accepted after the reviewer-rejection budget was exhausted. - [JsonPropertyName("completionRejectionBudgetExhausted")] - public bool? CompletionRejectionBudgetExhausted { get; set; } - - /// 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; } + /// + [JsonIgnore] + public override string Kind => "denied-interactively-by-user"; - /// Whether the rationale was derived from completion-reviewer output. - [JsonPropertyName("reviewerDerived")] - public bool? ReviewerDerived { get; set; } + /// Optional feedback from the user explaining the denial. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("feedback")] + public string? Feedback { get; set; } - /// Information-flow metadata captured from the completion reviewer. - [JsonPropertyName("reviewerResultMeta")] - public JsonElement? ReviewerResultMeta { get; set; } + /// Whether to force-reject the current agent turn. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("forceReject")] + public bool? ForceReject { get; set; } } -/// Expanded canonical result returned by a session tool. +/// 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 sealed class ToolResultExpanded +public partial class PermissionDecisionDeniedByContentExclusionPolicy : PermissionDecision { - /// Base64-encoded binary results returned to the model. - [JsonPropertyName("binaryResultsForLlm")] - public IList? BinaryResultsForLlm { get; set; } - - /// Sources returned by the tool that the model may cite. - [JsonPropertyName("citableSources")] - public IList? CitableSources { get; set; } - - /// Structured content blocks returned to the model. - [JsonPropertyName("contents")] - public IList? Contents { get; set; } - - /// Error message for an unsuccessful execution. - [JsonPropertyName("error")] - public string? Error { 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; } + /// + [JsonIgnore] + public override string Kind => "denied-by-content-exclusion-policy"; - /// Tool-specific telemetry payload. - [JsonPropertyName("toolTelemetry")] - public JsonElement? ToolTelemetry { get; set; } + /// Human-readable explanation of why the path was excluded. + [JsonPropertyName("message")] + public required string Message { get; set; } - /// Optional UI resource produced by the tool. - [JsonPropertyName("uiResource")] - public JsonElement? UiResource { get; set; } + /// File path that triggered the exclusion. + [JsonPropertyName("path")] + public required string Path { get; set; } } -/// Task-completion tool arguments and final result used to build a label-safe session event payload. +/// 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)] -internal sealed class ToolsTaskCompleteEventDataRequest +public partial class PermissionDecisionDeniedByPermissionRequestHook : PermissionDecision { - /// 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; + /// + [JsonIgnore] + public override string Kind => "denied-by-permission-request-hook"; - /// Arguments supplied to the completed task_complete tool call. - [JsonPropertyName("toolArgs")] - public JsonElement ToolArgs { get; set; } -} + /// Whether to interrupt the current agent turn. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("interrupt")] + public bool? Interrupt { get; set; } -/// Indicates whether the external tool call result was handled successfully. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HandlePendingToolCallResult -{ - /// Whether the tool call result was handled successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Optional message from the hook explaining the denial. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } } -/// Pending external tool call request ID, with the tool result or an error describing why it failed. +/// Pending permission request ID and the decision to apply (approve/reject and scope). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class HandlePendingToolCallRequest +internal sealed class PermissionDecisionRequest { - /// Error message if the tool call failed. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// 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 tool call. + /// Request ID of the pending permission request. [JsonPropertyName("requestId")] public string RequestId { get; set; } = string.Empty; - /// Tool call result (string or expanded result object). + /// The client's response to the pending permission prompt. [JsonPropertyName("result")] - public JsonElement? Result { get; set; } + public PermissionDecision Result { get => field ??= new(); set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// 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. +/// Pending permission prompt reconstructed from event history, with request ID and user-facing prompt details. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolsInitializeAndValidateResult +public sealed class PendingPermissionRequest { + /// 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; } -/// Identifies the target session. +/// List of pending permission requests reconstructed from event history. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionToolsInitializeAndValidateRequest +public sealed class PendingPermissionRequestList { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } } -/// Lightweight metadata for a currently initialized session tool. +/// No parameters; returns currently-pending permission requests for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CurrentToolMetadata +internal sealed class PermissionsPendingRequestsRequest { - /// 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; - - /// 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. +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolsGetCurrentMetadataResult +public sealed class PermissionsSetApproveAllResult { - /// Current tool metadata, or null when tools have not been initialized yet. - [JsonPropertyName("tools")] - public IList? Tools { get; set; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Identifies the target session. +/// Allow-all toggle for tool permission requests, with an optional telemetry source. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionToolsGetCurrentMetadataRequest +internal sealed class PermissionsSetApproveAllRequest { + /// Whether to auto-approve all tool permission requests. + [JsonPropertyName("enabled")] + public bool Enabled { 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; } } -/// Empty result after replacing the calling connection's externally implemented tools. +/// Indicates whether the requested permission mode was applied and reports the authoritative post-mutation mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolsSetResult +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; } } -/// Serializable definition of a caller-implemented tool whose execution is handled over the SDK connection. +/// Permission mode to apply for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ProtocolExternalToolDefinition +internal sealed class PermissionsSetModeRequest { - /// Tool-loading deferral policy. - [JsonPropertyName("defer")] - public ProtocolExternalToolDefer? Defer { get; set; } - - /// Model-visible explanation of what the tool does. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; - - /// Whether the tool executes commands in a terminal. - [JsonPropertyName("isTerminal")] - public bool? IsTerminal { 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; + /// 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; } - /// Whether this definition replaces a built-in tool with the same name. - [JsonPropertyName("overridesBuiltInTool")] - public bool? OverridesBuiltInTool { get; set; } + /// Permission mode to apply. + [JsonPropertyName("mode")] + public PermissionMode Mode { get; set; } - /// JSON Schema describing the tool's input arguments. - [JsonPropertyName("parameters")] - public IDictionary? Parameters { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Whether execution bypasses the normal tool permission prompt. - [JsonPropertyName("skipPermission")] - public bool? SkipPermission { get; set; } + /// 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; } +} - /// Optional human-readable display title. - [JsonPropertyName("title")] - public string? Title { get; set; } +/// Current permission mode. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionsGetModeResult +{ + /// Current permission mode. + [JsonPropertyName("mode")] + public PermissionMode Mode { get; set; } } -/// Complete externally implemented tool list for the calling connection. An empty list removes every tool previously supplied by that connection. +/// No parameters. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ToolsSetRequest +internal sealed class PermissionsGetModeRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Complete replacement list for the calling connection. - [JsonPropertyName("tools")] - public IList Tools { get => field ??= []; set; } } -/// Empty result after applying subagent settings. +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ToolsUpdateSubagentSettingsResult +public sealed class PermissionsModifyRulesResult { + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Subagent model, reasoning effort, context tier, and auto-invocation settings. +/// Scope and add/remove instructions for modifying session- or location-scoped permission rules. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SubagentSettingsEntry +internal sealed class PermissionsModifyRulesParams { - /// 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; } + /// Rules to add to the scope. Applied before `remove`/`removeAll`. + [JsonPropertyName("add")] + public IList? Add { get; set; } - /// Context tier override for matching subagents. - [JsonPropertyName("contextTier")] - public SubagentSettingsEntryContextTier? ContextTier { get; set; } + /// Specific rules to remove from the scope. Ignored when `removeAll` is true. + [JsonPropertyName("remove")] + public IList? Remove { get; set; } - /// Reasoning effort override for matching subagents. - [JsonPropertyName("effortLevel")] - public string? EffortLevel { 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; } - /// Model override for matching subagents. - [JsonPropertyName("model")] - public string? Model { 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; } - /// Whether the configured model strategy is preferred or required. - [JsonPropertyName("modelPolicy")] - public AgentModelPolicy? ModelPolicy { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Configured per-agent subagent overrides. -public sealed class UpdateSubagentSettingsRequestSubagents +/// Indicates whether the operation succeeded. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionsSetRequiredResult { - /// 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; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Subagent settings to apply to the current session. +/// Toggles whether permission prompts should be bridged into session events for this client. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UpdateSubagentSettingsRequest +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; - - /// Subagent settings to apply, or null to clear the live session override. - [JsonPropertyName("subagents")] - public UpdateSubagentSettingsRequestSubagents? Subagents { get; set; } } -/// RPC data type for SessionCommandsList operations. +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionCommandsListRequest +public sealed class PermissionsResetSessionApprovalsResult { - /// 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; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// RPC data type for SessionCommandsListRequestWithSession operations. +/// 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 SessionCommandsListRequestWithSession +internal sealed class PermissionsResetSessionApprovalsRequest { - /// 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; } + /// Whether location-scoped approvals and exact session-approved paths are cleared too. Defaults to `true`. + [JsonPropertyName("includeLocation")] + public bool? IncludeLocation { get; set; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). -/// Polymorphic base type discriminated by kind. +/// Indicates whether the operation succeeded. [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 +public sealed class PermissionsNotifyPromptShownResult { - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } - -/// Slash-command invocation result containing text output plus Markdown/ANSI rendering flags. -/// The text variant of . +/// Notification payload describing the permission prompt that the client just rendered. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultText : SlashCommandInvocationResult +internal sealed class PermissionPromptShownNotification { - /// - [JsonIgnore] - public override string Kind => "text"; - - /// Whether text contains Markdown. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("markdown")] - public bool? Markdown { get; set; } - - /// 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; } + /// 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; - /// Text output for the client to render. - [JsonPropertyName("text")] - public required string Text { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// 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 . +/// Snapshot of the session's recursive directory grants, exact session-approved paths, and primary working directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultAgentPrompt : SlashCommandInvocationResult +public sealed class PermissionPathsList { - /// - [JsonIgnore] - 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; } + /// All directories currently allowed for tool access on this session. + [JsonPropertyName("directories")] + public IList Directories { get => field ??= []; set; } - /// Prompt to submit to the agent. - [JsonPropertyName("prompt")] - public required string Prompt { get; set; } + /// The primary working directory for this session. + [JsonPropertyName("primary")] + public string Primary { get; set; } = string.Empty; - /// True when the invocation mutated user runtime settings; consumers caching settings should refresh. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("runtimeSettingsChanged")] - public bool? RuntimeSettingsChanged { get; set; } + /// Exact paths approved for this session without recursively allowing their descendants. + [JsonPropertyName("sessionApprovedPaths")] + public IList? SessionApprovedPaths { get; set; } } -/// Slash-command invocation result indicating completion, with optional message and settings-change flag. -/// The completed variant of . +/// No parameters; returns the session's recursive directory grants and exact session-approved paths. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultCompleted : SlashCommandInvocationResult +internal sealed class PermissionsPathsListRequest { - /// - [JsonIgnore] - public override string Kind => "completed"; - - /// Optional user-facing message describing the completed command. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("message")] - public string? Message { 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Selectable slash-command subcommand option with name, description, and optional group label. +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SlashCommandSelectSubcommandOption +public sealed class PermissionsPathsAddResult { - /// Human-readable description of the subcommand. - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; - - /// 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; + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Slash-command invocation result asking the client to present subcommand options for a parent command. -/// The select-subcommand variant of . +/// Directory path to add to the session's allowed directories. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultSelectSubcommand : SlashCommandInvocationResult +internal sealed class PermissionPathsAddParams { - /// - [JsonIgnore] - 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; } + /// 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; - /// Human-readable title for the selection UI. - [JsonPropertyName("title")] - public required string Title { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// RPC data type for SlashCommandTimelineEntry operations. +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SlashCommandTimelineEntry +public sealed class PermissionsPathsUpdatePrimaryResult { - /// 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; } - - /// 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; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// The add-timeline-entry variant of . +/// Directory path to set as the session's new primary working directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultAddTimelineEntry : SlashCommandInvocationResult +internal sealed class PermissionPathsUpdatePrimaryParams { - /// - [JsonIgnore] - public override string Kind => "add-timeline-entry"; - - /// 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("prefillInput")] - public string? PrefillInput { get; set; } + /// 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 command execution changed persisted runtime settings. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("runtimeSettingsChanged")] - public bool? RuntimeSettingsChanged { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// RPC data type for SlashCommandModelPickerDialog operations. +/// Indicates whether the supplied path is within the session's allowed directories. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SlashCommandModelPickerDialog +public sealed class PermissionPathsAllowedCheckResult { - /// 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; } - - /// Model-selection target represented by the picker. - [JsonPropertyName("target")] - public string? Target { get; set; } + /// Whether the path is within the session's allowed directories. + [JsonPropertyName("allowed")] + public bool Allowed { get; set; } } -/// The show-dialog variant of . +/// Path to evaluate against the session's allowed directories. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultShowDialog : SlashCommandInvocationResult +internal sealed class PermissionPathsAllowedCheckParams { - /// - [JsonIgnore] - public override string Kind => "show-dialog"; - - /// Dialog the host should display. - [JsonPropertyName("dialog")] - public required SlashCommandModelPickerDialog Dialog { get; set; } + /// Path to check against the session's allowed directories. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Whether command execution changed persisted runtime settings. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("runtimeSettingsChanged")] - public bool? RuntimeSettingsChanged { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// User-settings snapshot to restore if the host cancels the model switch. -public sealed class SlashCommandInvocationResultSetModelRevertOnCancel +/// Indicates whether the supplied path is within the session's workspace directory. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class PermissionPathsWorkspaceCheckResult { + /// Whether the path is within the session workspace directory. + [JsonPropertyName("allowed")] + public bool Allowed { get; set; } } -/// The set-model variant of . +/// Path to evaluate against the session's workspace (primary) directory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultSetModel : SlashCommandInvocationResult +internal sealed class PermissionPathsWorkspaceCheckParams { - /// - [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; } + /// Path to check against the session workspace directory. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Repository settings scope modified by the command. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("repoScope")] - public string? RepoScope { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// User-settings snapshot to restore if the host cancels the model switch. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("revertOnCancel")] - public SlashCommandInvocationResultSetModelRevertOnCancel? RevertOnCancel { 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; - /// Whether command execution changed persisted runtime settings. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("runtimeSettingsChanged")] - public bool? RuntimeSettingsChanged { get; set; } + /// Whether the location is a git repo or directory. + [JsonPropertyName("locationType")] + public PermissionLocationType LocationType { get; set; } +} - /// Settings scope modified by the command. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("scope")] - public string? Scope { 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; - /// User-facing warning produced while selecting the model. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("warning")] - public string? Warning { get; set; } + /// Working directory whose permission location should be resolved. + [JsonPropertyName("workingDirectory")] + public string WorkingDirectory { get; set; } = string.Empty; } -/// The set-plan-model variant of . +/// Summary of persisted location permissions applied to the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SlashCommandInvocationResultSetPlanModel : SlashCommandInvocationResult +public sealed class PermissionLocationApplyResult { - /// - [JsonIgnore] - public override string Kind => "set-plan-model"; + /// Number of persisted allowed directories added to the live path manager. + [JsonPropertyName("appliedDirectoryCount")] + public long AppliedDirectoryCount { get; set; } - /// User-facing confirmation message for the plan-model selection. - [JsonPropertyName("message")] - public required string Message { get; set; } + /// Number of location-scoped rules added to the live permission service. + [JsonPropertyName("appliedRuleCount")] + public long AppliedRuleCount { get; set; } - /// Dedicated model selected for plan mode. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("planModel")] - public string? PlanModel { get; set; } + /// Location-scoped rules applied to the live permission service. + [JsonPropertyName("appliedRules")] + public IList AppliedRules { get => field ??= []; set; } - /// Whether command execution changed persisted runtime settings. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("runtimeSettingsChanged")] - public bool? RuntimeSettingsChanged { get; 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; } } -/// Slash command name and optional raw input string to invoke. +/// Working directory to load persisted location permissions for. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CommandsInvokeRequest +internal sealed class PermissionLocationApplyParams { - /// 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; - - /// Optional client surface that initiated the invocation. - [JsonPropertyName("origin")] - public CommandsInvocationOrigin? Origin { get; set; } - /// 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; } -/// Whether finalizing the invocation effect succeeded, and the failure reason when it did not. +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CommandsFinalizeInvocationEffectResult +public sealed class PermissionsLocationsAddToolApprovalResult { - /// 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. + /// Whether the operation succeeded. [JsonPropertyName("success")] public bool Success { get; set; } } -/// The slash-command result object that produced the pending effect, echoed back unchanged. -public sealed class CommandsFinalizeInvocationEffectRequestEffect +/// Tool approval to persist and apply. +/// 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 { + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// The pending slash-command invocation effect to finalize, plus whether the host applied or cancelled it. + +/// Location-persisted tool approval details for specific command identifiers. +/// The commands variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CommandsFinalizeInvocationEffectRequest +public partial class PermissionsLocationsAddToolApprovalDetailsCommands : PermissionsLocationsAddToolApprovalDetails { - /// The slash-command result object that produced the pending effect, echoed back unchanged. - [JsonPropertyName("effect")] - public CommandsFinalizeInvocationEffectRequestEffect Effect { get => field ??= new(); set; } + /// + [JsonIgnore] + public override string Kind => "commands"; - /// Whether the host applied or cancelled the pending invocation effect. - [JsonPropertyName("outcome")] - public CommandsInvocationEffectOutcome Outcome { 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-persisted tool approval details for read-only filesystem operations. +/// The read variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class PermissionsLocationsAddToolApprovalDetailsRead : PermissionsLocationsAddToolApprovalDetails +{ + /// + [JsonIgnore] + public override string Kind => "read"; } -/// Indicates whether the pending client-handled command was completed successfully. +/// Location-persisted tool approval details for filesystem write operations. +/// The write variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CommandsHandlePendingCommandResult +public partial class PermissionsLocationsAddToolApprovalDetailsWrite : PermissionsLocationsAddToolApprovalDetails { - /// Whether the command was handled successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// + [JsonIgnore] + public override string Kind => "write"; } -/// Pending command request ID and an optional error if the client handler failed. +/// 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)] -internal sealed class CommandsHandlePendingCommandRequest +public partial class PermissionsLocationsAddToolApprovalDetailsMcp : PermissionsLocationsAddToolApprovalDetails { - /// Error message if the command handler failed. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// + [JsonIgnore] + public override string Kind => "mcp"; - /// Request ID from the command invocation event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// MCP server name. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// MCP tool name, or null to cover every tool on the server. + [JsonPropertyName("toolName")] + public string? ToolName { get; set; } } -/// Error message produced while executing the command, if any. +/// Location-persisted tool approval details for MCP sampling requests from a server. +/// The mcp-sampling variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ExecuteCommandResult +public partial class PermissionsLocationsAddToolApprovalDetailsMcpSampling : PermissionsLocationsAddToolApprovalDetails { - /// Error message produced while executing the command, if any. Omitted when the handler succeeded. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// + [JsonIgnore] + public override string Kind => "mcp-sampling"; + + /// MCP server name. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } } -/// Slash command name and argument string to execute synchronously. +/// Location-persisted tool approval details for writes to long-term memory. +/// The memory variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ExecuteCommandParams +public partial class PermissionsLocationsAddToolApprovalDetailsMemory : PermissionsLocationsAddToolApprovalDetails { - /// Argument string to pass to the command (empty string if none). - [JsonPropertyName("args")] - public string Args { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "memory"; +} - /// Name of the slash command to invoke (without the leading '/'). - [JsonPropertyName("commandName")] - public string CommandName { get; set; } = string.Empty; +/// Location-persisted tool 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 +{ + /// + [JsonIgnore] + public override string Kind => "custom-tool"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Custom tool name. + [JsonPropertyName("toolName")] + public required string ToolName { get; set; } } -/// Indicates whether the command was accepted into the local execution queue. -/// Data type discriminated by queued. +/// 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 partial class EnqueueCommandResult +public partial class PermissionsLocationsAddToolApprovalDetailsExtensionManagement : PermissionsLocationsAddToolApprovalDetails { - /// The boolean discriminator. - [JsonPropertyName("queued")] - public bool Queued { get; set; } + /// + [JsonIgnore] + public override string Kind => "extension-management"; - /// Stable opaque ID of the queued command. + /// Optional operation identifier; when omitted, the approval covers all extension management operations. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("queueId")] - public string? QueueId { get; set; } + [JsonPropertyName("operation")] + public string? Operation { get; set; } } -/// Slash-prefixed command string to enqueue for FIFO processing. +/// Location-persisted workflow approval, optionally narrowed by approval key. +/// The workflow variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class EnqueueCommandParams +public partial class PermissionsLocationsAddToolApprovalDetailsWorkflow : PermissionsLocationsAddToolApprovalDetails { - /// 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; - - /// Optional user-facing text for the queue row. The command string is shown when omitted. - [JsonPropertyName("displayText")] - public string? DisplayText { get; set; } + /// + [JsonIgnore] + public override string Kind => "workflow"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { 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; } } -/// Indicates whether the queued-command response was matched to a pending request. +/// 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)] -public sealed class CommandsRespondToQueuedCommandResult +public partial class PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess : PermissionsLocationsAddToolApprovalDetails { - /// 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; } + /// + [JsonIgnore] + public override string Kind => "extension-permission-access"; + + /// Extension name. + [JsonPropertyName("extensionName")] + public required string ExtensionName { get; set; } } -/// Result of the queued command execution. -/// Data type discriminated by handled. +/// 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 partial class QueuedCommandResult +public partial class PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess : PermissionsLocationsAddToolApprovalDetails { - /// The boolean discriminator. - [JsonPropertyName("handled")] - public bool Handled { get; set; } + /// + [JsonIgnore] + public override string Kind => "extension-env-access"; - /// 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; } + /// 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; } } -/// Queued-command request ID and the result indicating whether the host executed it (and whether to stop processing further queued commands). +/// Location-scoped tool approval to persist. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class CommandsRespondToQueuedCommandRequest +internal sealed class PermissionLocationAddToolApprovalParams { - /// Request ID from the `command.queued` event the host is responding to. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Tool approval to persist and apply. + [JsonPropertyName("approval")] + public PermissionsLocationsAddToolApprovalDetails Approval { get => field ??= new(); 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 string LocationKey { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Telemetry engagement ID for the session, when available. +/// Folder trust check result. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionTelemetryEngagement +public sealed class FolderTrustCheckResult { - /// Current telemetry engagement ID, when available. - [JsonPropertyName("engagementId")] - public string? EngagementId { get; set; } + /// Whether the folder is trusted. + [JsonPropertyName("trusted")] + public bool Trusted { get; set; } } -/// Identifies the target session. +/// Folder path to check for trust. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionTelemetryGetEngagementIdRequest +internal sealed class FolderTrustCheckParams { + /// Folder path to check. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Feature override key/value pairs to attach to subsequent telemetry events from this session. +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class TelemetrySetFeatureOverridesRequest +public sealed class PermissionsFolderTrustAddTrustedResult { - /// 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; } + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// Folder path to add to trusted folders. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class FolderTrustAddParams +{ + /// 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; } -/// Completed transient query. Ordered chunks and the terminal outcome are also delivered through `ui.ephemeral_query` session events while it runs. +/// Indicates whether the operation succeeded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIEphemeralQueryResult +public sealed class PermissionsUrlsSetUnrestrictedModeResult { - /// Answer returned by the model. - [JsonPropertyName("answer")] - public string Answer { get; set; } = string.Empty; + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } } -/// Transient question to answer without adding it to conversation history. +/// Whether the URL-permission policy should run in unrestricted mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIEphemeralQueryRequest +internal sealed class PermissionUrlsSetUnrestrictedModeParams { - /// Question to answer from the current conversation context. - [JsonPropertyName("question")] - public string Question { 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; } -/// The elicitation response (accept with form values, decline, or cancel). +/// The repository the remote session targets. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIElicitationResponse +public sealed class MetadataSnapshotRemoteMetadataRepository { - /// MCP response metadata. - [JsonPropertyName("_meta")] - public IDictionary? Meta { get; set; } + /// The branch the remote session is operating on. + [JsonPropertyName("branch")] + public string Branch { get; set; } = string.Empty; - /// The user's response: accept (submitted), decline (rejected), or cancel (dismissed). - [JsonPropertyName("action")] - public UIElicitationResponseAction Action { get; set; } + /// The GitHub repository name (without owner). + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// The form values submitted by the user (present when action is 'accept'). - [JsonPropertyName("content")] - public IDictionary? Content { get; set; } + /// The GitHub owner (user or organization) of the target repository. + [JsonPropertyName("owner")] + public string Owner { get; set; } = string.Empty; } -/// JSON Schema describing the form fields to present to the user. +/// 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 UIElicitationSchema +public sealed class MetadataSnapshotRemoteMetadata { - /// Form field definitions, keyed by field name. - [JsonPropertyName("properties")] - public IDictionary Properties { get => field ??= new Dictionary(); set; } + /// The pull request number the remote session is associated with, if any. + [JsonPropertyName("pullRequestNumber")] + public long? PullRequestNumber { get; set; } - /// List of required field names. - [JsonPropertyName("required")] - public IList? Required { get; set; } + /// The repository the remote session targets. + [JsonPropertyName("repository")] + public MetadataSnapshotRemoteMetadataRepository Repository { get => field ??= new(); set; } - /// Schema type indicator (always 'object'). - [JsonPropertyName("type")] - public string Type { get; set; } = string.Empty; -} + /// 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; } -/// 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; } + /// Whether the remote task originated from Copilot Coding Agent (cca) or a CLI `--remote` invocation. + [JsonPropertyName("taskType")] + public MetadataSnapshotRemoteMetadataTaskType? TaskType { get; set; } } -/// Prompt message and JSON schema describing the form fields to elicit from the user. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIElicitationRequest +/// Public-facing projection of workspace metadata for SDK / TUI consumers. +public sealed class SessionMetadataSnapshotWorkspace { - /// 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; + /// Branch checked out at session start, if any. + [JsonPropertyName("branch")] + public string? Branch { get; set; } - /// Elicitation mode. Omitted and form are equivalent for structured elicitation. - [JsonPropertyName("mode")] - public McpElicitationFormMode? Mode { get; set; } + /// ISO 8601 timestamp when the workspace was created. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } - /// JSON Schema describing the form fields to present to the user. - [JsonPropertyName("requestedSchema")] - public UIElicitationSchema RequestedSchema { get => field ??= new(); set; } + /// Current working directory at session start. + [JsonPropertyName("cwd")] + public string? Cwd { get; set; } - /// 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; } - /// MCP task metadata. - [JsonPropertyName("task")] - public McpTaskMetadata? Task { get; set; } -} + /// Repository host type, if known. + [JsonPropertyName("host_type")] + public WorkspaceSummaryHostType? HostType { get; set; } -/// Indicates whether the elicitation response was accepted; false if it was already resolved by another client. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIElicitationResult -{ - /// Whether the response was accepted. False if the request was already resolved by another client. - [JsonPropertyName("success")] - public bool Success { 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; -/// Pending elicitation request ID and the user's response (accept/decline/cancel + form values). -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIHandlePendingElicitationRequest -{ - /// The unique request ID from the elicitation.requested event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Display name for the session, if set. + [JsonPropertyName("name")] + public string? Name { get; set; } - /// The elicitation response (accept with form values, decline, or cancel). - [JsonPropertyName("result")] - public UIElicitationResponse Result { get => field ??= new(); set; } + /// Repository identifier in 'owner/repo' or 'org/project/repo' format, if any. + [JsonPropertyName("repository")] + public string? Repository { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// ISO 8601 timestamp when the workspace was last updated. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } -/// Indicates whether the pending UI request was resolved by this call. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIHandlePendingResult -{ - /// 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; } + /// Whether the display name was explicitly set by the user. + [JsonPropertyName("user_named")] + public bool? UserNamed { get; set; } } -/// User response for a pending user-input request, with answer text and whether it was typed freeform. +/// Point-in-time snapshot of slow-changing session identifier and state fields. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIUserInputResponse +public sealed class SessionMetadataSnapshot { - /// The user's answer text. - [JsonPropertyName("answer")] - public string Answer { get; set; } = string.Empty; + /// 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; } - /// 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; } -} + /// Runtime client name associated with the session (telemetry identifier). + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } -/// Request ID of a pending `user_input.requested` event and the user's response. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIHandlePendingUserInputRequest -{ - /// The unique request ID from the user_input.requested event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// The current agent mode for this session (e.g., 'interactive', 'plan', 'autopilot'). + [JsonPropertyName("currentMode")] + public MetadataSnapshotCurrentMode CurrentMode { get; set; } - /// 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; } + /// 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; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { 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; } -/// Optional sampling result payload. Omit to reject/cancel the sampling request without providing a result. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIHandlePendingSamplingResponse -{ -} + /// 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; } -/// 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 UIHandlePendingSamplingRequest -{ - /// The unique request ID from the sampling.requested event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// 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; } - /// Optional sampling result payload. Omit to reject/cancel the sampling request without providing a result. - [JsonPropertyName("response")] - public UIHandlePendingSamplingResponse? Response { 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; } - /// Target session identifier. + /// 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; -} -/// Request ID of a pending `auto_mode_switch.requested` event and the user's response. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIHandlePendingAutoModeSwitchRequest -{ - /// The unique request ID from the auto_mode_switch.requested event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Current session limits, or null when no limits are active. + [JsonPropertyName("sessionLimits")] + public SessionLimitsConfig? SessionLimits { 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; } + /// ISO 8601 timestamp of when the session started. + [JsonPropertyName("startTime")] + public DateTimeOffset StartTime { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Short human-readable summary of the session, if known. Omitted when no summary has been generated. + [JsonPropertyName("summary")] + public string? Summary { get; set; } -/// The user's selected action for an exhausted session limit. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UISessionLimitsExhaustedResponse -{ - /// Action selected by the user. - [JsonPropertyName("action")] - public UISessionLimitsExhaustedResponseAction Action { get; set; } + /// Absolute path to the session's current working directory. + [JsonPropertyName("workingDirectory")] + public string WorkingDirectory { get; set; } = string.Empty; - /// AI Credits to add to the current max when action is 'add'. - [JsonPropertyName("additionalAiCredits")] - public double? AdditionalAiCredits { 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; } - /// New absolute max AI Credits when action is 'set'. - [JsonPropertyName("maxAiCredits")] - public double? MaxAiCredits { get; 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; } } -/// Request ID of a pending `session_limits_exhausted.requested` event and the user's selected limit action. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class UIHandlePendingSessionLimitsExhaustedRequest +internal sealed class SessionMetadataSnapshotRequest { - /// 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; } -/// User response for a pending exit-plan-mode request, with approval state, selected action, auto-approve flag, and feedback. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIExitPlanModeResponse +internal sealed class SessionMetadataGetClientMetadataRequest { - /// 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Request ID of a pending `exit_plan_mode.requested` event and the user's response. +/// 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 UIHandlePendingExitPlanModeRequest +internal sealed class MetadataUpdateClientMetadataRequest { - /// The unique request ID from the exit_plan_mode.requested event. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Remove every existing client metadata entry before applying remove and set. Defaults to false. + [JsonPropertyName("clear")] + public bool? Clear { 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; } + /// 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; } } -/// 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 local session is currently processing a turn or background continuation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIRegisterDirectAutoModeSwitchHandlerResult +public sealed class MetadataIsProcessingResult { - /// 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 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 SessionUiRegisterDirectAutoModeSwitchHandlerRequest +internal sealed class SessionMetadataIsProcessingRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the handle was active and the registration count was decremented. +/// Current activity flags for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UIUnregisterDirectAutoModeSwitchHandlerResult +public sealed class SessionActivity { - /// True if the handle was active and decremented the counter; false if the handle was unknown. - [JsonPropertyName("unregistered")] - public bool Unregistered { get; set; } + /// 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; } } -/// 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 SessionMetadataActivityRequest { - /// 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. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsConfigureResult +/// Token-usage breakdown for the session's current context window. +public sealed class MetadataContextInfoResultContextInfo { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } -} + /// Output reserve plus tokens after the buffer-exhaustion blocking threshold (default 95%). + [JsonPropertyName("bufferTokens")] + public long BufferTokens { 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. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Token count at which background compaction starts (configurable percentage of promptTokenLimit). + [JsonPropertyName("compactionThreshold")] + public long CompactionThreshold { get; set; } - /// Type of the policy source. - [JsonPropertyName("type")] - public string Type { get; set; } = string.Empty; -} + /// Tokens consumed by user/assistant/tool messages. + [JsonPropertyName("conversationTokens")] + public long ConversationTokens { get; set; } -/// Single content-exclusion rule supplied to `session.permissions.configure`, with paths, match conditions, and source. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsConfigureAdditionalContentExclusionPolicyRule -{ - /// Conditions of which at least one must match. - [JsonPropertyName("ifAnyMatch")] - public IList? IfAnyMatch { get; set; } + /// Prompt token limit plus the model's full output token limit. + [JsonPropertyName("limit")] + public long Limit { get; set; } - /// Conditions none of which may match. - [JsonPropertyName("ifNoneMatch")] - public IList? IfNoneMatch { get; set; } + /// Tokens consumed by MCP tool definitions (subset of toolDefinitionsTokens, excludes deferred tools). + [JsonPropertyName("mcpToolsTokens")] + public long McpToolsTokens { get; set; } - /// Path patterns covered by this rule. - [JsonPropertyName("paths")] - public IList Paths { get => field ??= []; set; } + /// The model used for token counting. + [JsonPropertyName("modelName")] + public string ModelName { 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; } -} + /// Maximum prompt tokens allowed by the model (or DEFAULT_TOKEN_LIMIT if unspecified). + [JsonPropertyName("promptTokenLimit")] + public long PromptTokenLimit { get; set; } -/// Content-exclusion policy supplied to `session.permissions.configure`, with rules, last-updated data, and scope. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsConfigureAdditionalContentExclusionPolicy -{ - /// Opaque policy update timestamp supplied by the host. - [JsonPropertyName("last_updated_at")] - public JsonElement LastUpdatedAt { get; set; } + /// Tokens consumed by the system prompt. + [JsonPropertyName("systemTokens")] + public long SystemTokens { get; set; } - /// Content-exclusion rules to apply. - [JsonPropertyName("rules")] - public IList Rules { get => field ??= []; set; } + /// Tokens consumed by tool definitions sent to the model (excludes deferred tools). + [JsonPropertyName("toolDefinitionsTokens")] + public long ToolDefinitionsTokens { get; set; } - /// Allowed values for the `PermissionsConfigureAdditionalContentExclusionPolicyScope` enumeration. - [JsonPropertyName("scope")] - public PermissionsConfigureAdditionalContentExclusionPolicyScope Scope { get; set; } + /// Sum of system, conversation and tool-definition tokens. + [JsonPropertyName("totalTokens")] + public long TotalTokens { 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. +/// Token breakdown for the session's current context window, or null if uninitialized. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionPathsConfig +public sealed class MetadataContextInfoResult { - /// 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; } - - /// 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; } - - /// If true, the runtime allows access to all paths without prompting. Equivalent to constructing an UnrestrictedPathManager. - [JsonPropertyName("unrestricted")] - public bool? Unrestricted { 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; } + /// 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; } } -/// If specified, replaces the session's approved/denied permission rules. Omit to leave the current rules unchanged. +/// Model identifier and token limits used to compute the context-info breakdown. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionRulesSet +internal sealed class MetadataContextInfoRequest { - /// Rules that auto-approve matching requests. - [JsonPropertyName("approved")] - public IList Approved { get => field ??= []; set; } + /// Maximum output tokens allowed by the target model. Pass 0 if unknown. + [JsonPropertyName("outputTokenLimit")] + public long OutputTokenLimit { get; set; } - /// Rules that auto-deny matching requests. - [JsonPropertyName("denied")] - public IList Denied { get => field ??= []; set; } -} + /// Maximum prompt tokens allowed by the target model. Pass 0 to use the runtime default. + [JsonPropertyName("promptTokenLimit")] + public long PromptTokenLimit { get; 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. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionUrlsConfig -{ - /// Initial list of allowed URL/domain patterns. Patterns may include path components. Ignored when `unrestricted` is true. - [JsonPropertyName("initialAllowed")] - public IList? InitialAllowed { 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; } - /// 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Patch of permission policy fields to apply (omit a field to leave it unchanged). -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsConfigureParams +/// 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 { - /// 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; } - - /// 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; } + /// Output reserve plus post-blocking-threshold buffer. + [JsonPropertyName("buffer")] + public long Buffer { 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; } + /// Custom-instructions tokens (0 when none are configured). + [JsonPropertyName("customInstructions")] + public long CustomInstructions { 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; } + /// Remaining unused window capacity (clamped at 0). + [JsonPropertyName("freeSpace")] + public long FreeSpace { 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; } + /// MCP tool-definition tokens. + [JsonPropertyName("mcpTools")] + public long McpTools { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Conversation (user/assistant/tool) message tokens. + [JsonPropertyName("messages")] + public long Messages { get; 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. - [JsonPropertyName("urls")] - public PermissionUrlsConfig? Urls { get; set; } -} + /// System prompt tokens, excluding custom instructions. + [JsonPropertyName("systemPrompt")] + public long SystemPrompt { get; set; } -/// Indicates whether the permission decision was applied; false when the request was already resolved. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionRequestResult -{ - /// Whether the permission request was handled successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Non-MCP tool-definition tokens. + [JsonPropertyName("systemTools")] + public long SystemTools { get; set; } } -/// The client's response to the pending permission prompt. -/// Polymorphic base type discriminated by kind. -[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 +/// Successful compaction history for the session. +public sealed class MetadataContextAttributionResultContextAttributionCompactions { - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + /// Number of successful compactions in this session. + [JsonPropertyName("count")] + public long Count { 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 +/// RPC data type for MetadataContextAttributionResultContextAttributionEntry operations. +public sealed class MetadataContextAttributionResultContextAttributionEntry { - /// - [JsonIgnore] - public override string Kind => "approve-once"; + /// 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; } - /// 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; } -} + /// 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; -/// 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(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. + /// 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 virtual string Kind { get; set; } = string.Empty; -} + 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; -/// Session-scoped approval details for specific command identifiers. -/// The commands variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalCommands : PermissionDecisionApproveForSessionApproval -{ - /// - [JsonIgnore] - public override string Kind => "commands"; + /// 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; } - /// Command identifiers covered by this approval. - [JsonPropertyName("commandIdentifiers")] - public required IList CommandIdentifiers { get; set; } + /// Token count currently in context attributable to this entry. + [JsonPropertyName("tokens")] + public long Tokens { 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 +/// Per-source token attribution snapshot for the current context window. The heaviest individual messages are available separately via `metadata.getContextHeaviestMessages`. +public sealed class MetadataContextAttributionResultContextAttribution { - /// - [JsonIgnore] - public override string Kind => "read"; -} + /// Output reserve plus the tokens past the buffer-exhaustion blocking threshold. Mirrors `SessionContextInfo.bufferTokens`. + [JsonPropertyName("bufferTokens")] + public long BufferTokens { get; set; } -/// Session-scoped approval details for filesystem write operations. -/// The write variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalWrite : PermissionDecisionApproveForSessionApproval -{ - /// - [JsonIgnore] - public override string Kind => "write"; -} + /// 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; } -/// 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 PermissionDecisionApproveForSessionApprovalMcp : PermissionDecisionApproveForSessionApproval -{ - /// - [JsonIgnore] - public override string Kind => "mcp"; + /// Successful compaction history for the session. + [JsonPropertyName("compactions")] + public MetadataContextAttributionResultContextAttributionCompactions Compactions { get => field ??= new(); set; } - /// MCP server name. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// Token count at which background compaction starts. Mirrors `SessionContextInfo.compactionThreshold`. + [JsonPropertyName("compactionThreshold")] + public long CompactionThreshold { get; set; } - /// MCP tool name, or null to cover every tool on the server. - [JsonPropertyName("toolName")] - public string? ToolName { 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; } -/// 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 PermissionDecisionApproveForSessionApprovalMcpSampling : PermissionDecisionApproveForSessionApproval -{ - /// - [JsonIgnore] - public override string Kind => "mcp-sampling"; + /// 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; } - /// MCP server name. - [JsonPropertyName("serverName")] - public required string ServerName { 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; + + /// 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; + + /// 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; } + + /// 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; } } -/// Session-scoped approval details for writes to long-term memory. -/// The memory variant of . +/// Per-source attribution breakdown for the session's current context window, or null if uninitialized. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalMemory : PermissionDecisionApproveForSessionApproval +public sealed class MetadataContextAttributionResult { - /// - [JsonIgnore] - public override string Kind => "memory"; + /// 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; } } -/// Session-scoped approval details for a custom tool, keyed by tool name. -/// The custom-tool variant of . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalCustomTool : PermissionDecisionApproveForSessionApproval +internal sealed class SessionMetadataGetContextAttributionRequest { - /// - [JsonIgnore] - public override string Kind => "custom-tool"; - - /// Custom tool name. - [JsonPropertyName("toolName")] - public required string ToolName { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Session-scoped approval details for extension-management operations, optionally narrowed by operation. -/// The extension-management variant of . +/// A single large message currently in context. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalExtensionManagement : PermissionDecisionApproveForSessionApproval +public sealed class ContextHeaviestMessage { - /// - [JsonIgnore] - public override string Kind => "extension-management"; + /// Stable identifier for this message within the snapshot. + [JsonPropertyName("id")] + public string Id { 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; } + /// Human-readable source label, e.g. `tool: bash` or `skill: tmux`. Presentation-only. + [JsonPropertyName("label")] + public string Label { 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; } } -/// Session-scoped workflow approval, optionally narrowed by approval key. -/// The workflow variant of . +/// The heaviest individual messages in the session's context window, most-expensive first. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalWorkflow : PermissionDecisionApproveForSessionApproval +public sealed class MetadataContextHeaviestMessagesResult { - /// - [JsonIgnore] - public override string Kind => "workflow"; + /// Heaviest messages, most-expensive first. + [JsonPropertyName("messages")] + public IList Messages { get => field ??= []; 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; } + /// 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; } } -/// Session-scoped approval details for an extension's permission-gated capability access, keyed by extension name. -/// The extension-permission-access variant of . +/// Parameters for the heaviest-messages query. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSessionApprovalExtensionPermissionAccess : PermissionDecisionApproveForSessionApproval +internal sealed class MetadataContextHeaviestMessagesRequest { - /// - [JsonIgnore] - public override string Kind => "extension-permission-access"; + /// Maximum number of messages to return, most-expensive first. Omit for the server default. + [JsonPropertyName("limit")] + public long? Limit { get; set; } - /// Extension name. - [JsonPropertyName("extensionName")] - public required string ExtensionName { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// 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 . +/// 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)] -public partial class PermissionDecisionApproveForSessionApprovalExtensionEnvAccess : PermissionDecisionApproveForSessionApproval +public sealed class MetadataRecordContextChangeResult { - /// - [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; } } -/// 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 . +/// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForSession : PermissionDecision +public sealed class SessionWorkingDirectoryContext { - /// - [JsonIgnore] - public override string Kind => "approve-for-session"; + /// Merge-base commit SHA (fork point from the remote default branch). + [JsonPropertyName("baseCommit")] + public string? BaseCommit { get; set; } - /// Session-scoped approval to remember (tool prompts only; omitted for path/url prompts). - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("approval")] - public PermissionDecisionApproveForSessionApproval? Approval { get; set; } + /// Current git branch name. + [JsonPropertyName("branch")] + public string? Branch { get; set; } - /// URL domain to approve for the rest of the session (URL prompts only). - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("domain")] - public string? Domain { get; set; } -} + /// Current working directory path. + [JsonPropertyName("cwd")] + public string Cwd { 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"; + /// Root directory of the git repository, resolved via git rev-parse. + [JsonPropertyName("gitRoot")] + public string? GitRoot { get; set; } - /// Canonical directories covered by the read-only grant. - [JsonPropertyName("directories")] - public required IList Directories { get; set; } -} + /// Head commit of the current git branch. + [JsonPropertyName("headCommit")] + public string? HeadCommit { get; set; } -/// Approval to persist for this location. -/// Polymorphic base type discriminated by kind. -[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 -{ - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; -} + /// 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; } -/// Location-scoped approval details for specific command identifiers. -/// The commands variant of . + /// Raw host string from the git remote URL (e.g. "github.com", "dev.azure.com"). + [JsonPropertyName("repositoryHost")] + public string? RepositoryHost { get; set; } +} + +/// Updated working-directory/git context to record on the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalCommands : PermissionDecisionApproveForLocationApproval +internal sealed class MetadataRecordContextChangeRequest { - /// - [JsonIgnore] - public override string Kind => "commands"; + /// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. + [JsonPropertyName("context")] + public SessionWorkingDirectoryContext Context { get => field ??= new(); 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 . +/// 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 partial class PermissionDecisionApproveForLocationApprovalRead : PermissionDecisionApproveForLocationApproval +public sealed class MetadataSetWorkingDirectoryResult { - /// - [JsonIgnore] - public override string Kind => "read"; + /// Working directory after the update. + [JsonPropertyName("workingDirectory")] + public string WorkingDirectory { get; set; } = string.Empty; } -/// Location-scoped approval details for filesystem write operations. -/// The write variant of . +/// 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)] -public partial class PermissionDecisionApproveForLocationApprovalWrite : PermissionDecisionApproveForLocationApproval +internal sealed class MetadataSetWorkingDirectoryRequest { - /// - [JsonIgnore] - public override string Kind => "write"; + /// 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; } -/// Location-scoped approval details for an MCP server tool, or all tools on the server when `toolName` is null. -/// The mcp variant of . +/// 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)] -public partial class PermissionDecisionApproveForLocationApprovalMcp : PermissionDecisionApproveForLocationApproval +public sealed class MetadataRecomputeContextTokensResult { - /// - [JsonIgnore] - public override string Kind => "mcp"; + /// Tokens contributed by user/assistant/tool messages (excludes system/developer prompts). + [JsonPropertyName("messagesTokenCount")] + public long MessagesTokenCount { get; set; } - /// MCP server name. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// Tokens contributed by system/developer prompt snapshots. + [JsonPropertyName("systemTokenCount")] + public long SystemTokenCount { get; set; } - /// MCP tool name, or null to cover every tool on the server. - [JsonPropertyName("toolName")] - public string? ToolName { get; set; } + /// Sum of tokens across chat-context and system-context messages currently held by the session. + [JsonPropertyName("totalTokens")] + public long TotalTokens { get; set; } } -/// Location-scoped approval details for MCP sampling requests from a server. -/// The mcp-sampling variant of . +/// Model identifier to use when re-tokenizing the session's existing messages. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalMcpSampling : PermissionDecisionApproveForLocationApproval +internal sealed class MetadataRecomputeContextTokensRequest { - /// - [JsonIgnore] - public override string Kind => "mcp-sampling"; + /// 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; - /// MCP server name. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Location-scoped approval details for writes to long-term memory. -/// The memory variant of . +/// Availability of built-in job tools surfaced to boundary consumers. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalMemory : PermissionDecisionApproveForLocationApproval +public sealed class SessionSettingsBuiltInToolAvailabilitySnapshot { - /// - [JsonIgnore] - public override string Kind => "memory"; + /// 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; } } -/// Location-scoped approval details for a custom tool, keyed by tool name. -/// The custom-tool variant of . +/// Redacted job settings for a session. The job nonce is excluded. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalCustomTool : PermissionDecisionApproveForLocationApproval +public sealed class SessionSettingsJobSnapshot { - /// - [JsonIgnore] - public override string Kind => "custom-tool"; + /// Availability of job-specific built-in tools. + [JsonPropertyName("builtInToolAvailability")] + public SessionSettingsBuiltInToolAvailabilitySnapshot? BuiltInToolAvailability { get; set; } - /// Custom tool name. - [JsonPropertyName("toolName")] - public required string ToolName { get; set; } + /// GitHub Actions event type for the job. + [JsonPropertyName("eventType")] + public string? EventType { get; set; } + + /// Whether this is the workflow's trigger job. + [JsonPropertyName("isTriggerJob")] + public bool? IsTriggerJob { get; set; } } -/// Location-scoped approval details for extension-management operations, optionally narrowed by operation. -/// The extension-management variant of . +/// Redacted model routing settings for a session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalExtensionManagement : PermissionDecisionApproveForLocationApproval +public sealed class SessionSettingsModelSnapshot { - /// - [JsonIgnore] - public override string Kind => "extension-management"; + /// Agent service callback URL for job and progress updates. + [JsonPropertyName("callbackUrl")] + public string? CallbackUrl { get; set; } - /// Optional operation identifier; when omitted, the approval covers all extension management operations. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("operation")] - public string? Operation { get; set; } -} + /// Default reasoning effort for the selected model. + [JsonPropertyName("defaultReasoningEffort")] + public string? DefaultReasoningEffort { 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"; + /// Agent job identifier for the session. + [JsonPropertyName("instanceId")] + public string? InstanceId { 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; } + /// Selected model identifier. + [JsonPropertyName("model")] + public string? Model { get; set; } } -/// Location-scoped approval details for an extension's permission-gated capability access, keyed by extension name. -/// The extension-permission-access variant of . +/// Online-evaluation settings safe to expose across the SDK boundary. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalExtensionPermissionAccess : PermissionDecisionApproveForLocationApproval +public sealed class SessionSettingsOnlineEvaluationSnapshot { - /// - [JsonIgnore] - public override string Kind => "extension-permission-access"; + /// Whether online evaluation is disabled. + [JsonPropertyName("disableOnlineEvaluation")] + public bool? DisableOnlineEvaluation { get; set; } - /// Extension name. - [JsonPropertyName("extensionName")] - public required string ExtensionName { get; set; } + /// Whether online-evaluation output-file generation is enabled. + [JsonPropertyName("enableOnlineEvaluationOutputFile")] + public bool? EnableOnlineEvaluationOutputFile { 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 . +/// Redacted repository and GitHub host settings for a session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocationApprovalExtensionEnvAccess : PermissionDecisionApproveForLocationApproval +public sealed class SessionSettingsRepoSnapshot { - /// - [JsonIgnore] - public override string Kind => "extension-env-access"; + /// Checked-out repository branch. + [JsonPropertyName("branch")] + public string? Branch { get; set; } - /// Names of the sensitive environment variables this approval covers. Values are never persisted. - [JsonPropertyName("environmentVariables")] - public required IList EnvironmentVariables { get; set; } + /// Checked-out commit SHA. + [JsonPropertyName("commit")] + public string? Commit { get; set; } - /// Extension name. - [JsonPropertyName("extensionName")] - public required string ExtensionName { get; set; } -} + /// GitHub server host name. + [JsonPropertyName("host")] + public string? Host { 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 . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApproveForLocation : PermissionDecision -{ - /// - [JsonIgnore] - public override string Kind => "approve-for-location"; + /// Protocol used to access the GitHub host. + [JsonPropertyName("hostProtocol")] + public string? HostProtocol { get; set; } - /// Approval to persist for this location. - [JsonPropertyName("approval")] - public required PermissionDecisionApproveForLocationApproval Approval { get; set; } + /// GitHub repository database ID. + [JsonPropertyName("id")] + public double? Id { get; set; } - /// Location key (git root or cwd) to persist the approval to. - [JsonPropertyName("locationKey")] - public required string LocationKey { get; set; } -} + /// Repository name. + [JsonPropertyName("name")] + public string? Name { 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"; + /// GitHub repository owner database ID. + [JsonPropertyName("ownerId")] + public double? OwnerId { get; set; } - /// URL domain to approve permanently. - [JsonPropertyName("domain")] - public required string Domain { get; set; } -} + /// Repository owner login. + [JsonPropertyName("ownerName")] + public string? OwnerName { get; set; } -/// Permission-decision request variant to reject a pending permission request, with optional feedback. -/// The reject variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionReject : PermissionDecision -{ - /// - [JsonIgnore] - public override string Kind => "reject"; + /// Number of commits in the pull request. + [JsonPropertyName("prCommitCount")] + public double? PrCommitCount { get; set; } - /// Optional feedback explaining the rejection. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("feedback")] - public string? Feedback { get; set; } -} + /// Whether the repository is writable. + [JsonPropertyName("readWrite")] + public bool? ReadWrite { 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"; -} + /// GitHub secret-scanning service URL. + [JsonPropertyName("secretScanningUrl")] + public string? SecretScanningUrl { 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"; + /// GitHub server base URL. + [JsonPropertyName("serverUrl")] + public string? ServerUrl { get; set; } } -/// Permission-decision variant indicating approval was remembered for the session, with approval details. -/// The approved-for-session variant of . +/// Redacted validation and memory-tool settings for a session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionApprovedForSession : PermissionDecision +public sealed class SessionSettingsValidationSnapshot { - /// - [JsonIgnore] - public override string Kind => "approved-for-session"; + /// Whether advisory validation is enabled. + [JsonPropertyName("advisoryEnabled")] + public bool? AdvisoryEnabled { get; set; } - /// The approval to add as a session-scoped rule. - [JsonPropertyName("approval")] - public required UserToolSessionApproval Approval { 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; } + + /// General validation timeout budget in seconds. + [JsonPropertyName("timeout")] + public double? Timeout { get; set; } } -/// Permission-decision variant indicating approval was persisted for a project location, with approval details and location key. -/// The approved-for-location variant of . +/// 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 partial class PermissionDecisionApprovedForLocation : PermissionDecision +internal sealed class SessionSettingsSnapshot { - /// - [JsonIgnore] - public override string Kind => "approved-for-location"; + /// Name of the SDK client that created the session. + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } - /// The approval to persist for this location. - [JsonPropertyName("approval")] - public required UserToolSessionApproval Approval { get; set; } + /// Redacted job settings. + [JsonPropertyName("job")] + public SessionSettingsJobSnapshot Job { get => field ??= new(); set; } - /// The location key (git root or cwd) to persist the approval to. - [JsonPropertyName("locationKey")] - public required string LocationKey { get; set; } -} + /// Redacted model routing settings. + [JsonPropertyName("model")] + public SessionSettingsModelSnapshot Model { get => field ??= new(); 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"; + /// Online-evaluation settings safe for SDK consumers. + [JsonPropertyName("onlineEvaluation")] + public SessionSettingsOnlineEvaluationSnapshot OnlineEvaluation { get => field ??= new(); set; } - /// Optional explanation of why the request was cancelled. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("reason")] - public string? Reason { get; 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; } } -/// Permission-decision variant indicating explicit denial by permission rules, with the matching rules. -/// The denied-by-rules variant of . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionDeniedByRules : PermissionDecision +internal sealed class SessionSettingsSnapshotRequest { - /// - [JsonIgnore] - public override string Kind => "denied-by-rules"; - - /// Rules that denied the request. - [JsonPropertyName("rules")] - public required IList Rules { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// 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 . +/// Result of evaluating a Rust-owned settings predicate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser : PermissionDecision +internal sealed class SessionSettingsEvaluatePredicateResult { - /// - [JsonIgnore] - public override string Kind => "denied-no-approval-rule-and-could-not-request-from-user"; + /// Whether the named settings predicate evaluated to enabled. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } } -/// Permission-decision variant indicating the user denied an interactive prompt, with optional feedback and force-reject flag. -/// The denied-interactively-by-user variant of . +/// Named Rust-owned settings predicate to evaluate for this session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionDeniedInteractivelyByUser : PermissionDecision +internal sealed class SessionSettingsEvaluatePredicateRequest { - /// - [JsonIgnore] - public override string Kind => "denied-interactively-by-user"; + /// Predicate name. The runtime owns the raw feature-flag names and composition logic. + [JsonPropertyName("name")] + public SessionSettingsPredicateName Name { get; set; } - /// Optional feedback from the user explaining the denial. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("feedback")] - public string? Feedback { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; - /// Whether to force-reject the current agent turn. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("forceReject")] - public bool? ForceReject { get; set; } + /// Tool name for tool-scoped predicates such as trivial-change handling. + [JsonPropertyName("toolName")] + public string? ToolName { get; set; } } -/// Permission-decision variant indicating denial by content-exclusion policy, with path and message. -/// The denied-by-content-exclusion-policy variant of . +/// Content-exclusion decision for one requested path. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionDeniedByContentExclusionPolicy : PermissionDecision +public sealed class ContentExclusionPathCheck { - /// - [JsonIgnore] - public override string Kind => "denied-by-content-exclusion-policy"; - - /// Human-readable explanation of why the path was excluded. - [JsonPropertyName("message")] - public required string Message { get; set; } + /// Whether the session's complete content-exclusion policy excludes the path. + [JsonPropertyName("excluded")] + public bool Excluded { get; set; } - /// File path that triggered the exclusion. + /// The path supplied by the caller. [JsonPropertyName("path")] - public required string Path { get; set; } + public string Path { get; set; } = string.Empty; } -/// Permission-decision variant indicating denial by a permission request hook, with optional message and interrupt flag. -/// The denied-by-permission-request-hook variant of . +/// Batch content-exclusion result. Callers must fail closed when policy evaluation is unavailable. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionDecisionDeniedByPermissionRequestHook : PermissionDecision +public sealed class ContentExclusionCheckPathsResult { - /// - [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; } + /// 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; } - /// Optional message from the hook explaining the denial. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("message")] - public string? Message { get; set; } + /// Per-path decisions in request order. Empty when available is false. + [JsonPropertyName("checks")] + public IList Checks { get => field ??= []; set; } } -/// Pending permission request ID and the decision to apply (approve/reject and scope). +/// 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 PermissionDecisionRequest +internal sealed class ContentExclusionCheckPathsRequest { - /// 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; } + /// 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; } -/// Pending permission prompt reconstructed from event history, with request ID and user-facing prompt details. +/// Identifier of the spawned shell process, usable with shell.kill while the process is running. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PendingPermissionRequest +public sealed class ShellExecResult { - /// 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; + /// Identifier usable with shell.kill while the process is running. + [JsonPropertyName("processId")] + public string ProcessId { get; set; } = string.Empty; } -/// List of pending permission requests reconstructed from event history. +/// Shell command to run, with optional working directory and timeout in milliseconds. Spawn failures return an RPC error. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PendingPermissionRequestList +internal sealed class ShellExecRequest { - /// 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; } -} + /// 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; } -/// 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; + + /// Timeout in milliseconds (default: 30000). + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonPropertyName("timeout")] + public TimeSpan? Timeout { get; set; } } -/// Indicates whether the operation succeeded. +/// Indicates whether the signal was delivered; false if the process was unknown or already exited. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsSetApproveAllResult +public sealed class ShellKillResult { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Whether the signal was sent successfully. + [JsonPropertyName("killed")] + public bool Killed { get; set; } } -/// Allow-all toggle for tool permission requests, with an optional telemetry source. +/// Identifier of a process previously returned by "shell.exec" and the signal to send. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsSetApproveAllRequest +internal sealed class ShellKillRequest { - /// Whether to auto-approve all tool permission requests. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// 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; - /// Optional source for allow-all telemetry. Defaults to `rpc` when omitted for SDK callers. - [JsonPropertyName("source")] - public PermissionsSetApproveAllSource? Source { get; set; } + /// Signal to send (default: SIGTERM). + [JsonPropertyName("signal")] + public ShellKillSignal? Signal { get; set; } } -/// Indicates whether the requested permission mode was applied and reports the authoritative post-mutation mode. +/// Result of a user-requested shell command. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsSetModeResult +public sealed class UserRequestedShellCommandResult { - /// Authoritative permission mode after the mutation. - [JsonPropertyName("mode")] - public PermissionMode Mode { get; set; } + /// Error output when the execution failed. + [JsonPropertyName("error")] + public string? Error { get; set; } - /// Whether the operation succeeded. + /// 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; } -/// Permission mode to apply for the session. +/// User-requested shell command and cancellation handle. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsSetModeRequest +internal sealed class ShellExecuteUserRequestedRequest { - /// 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; } + /// Shell command to execute. + [JsonPropertyName("command")] + public string Command { get; set; } = string.Empty; - /// Permission mode to apply. - [JsonPropertyName("mode")] - public PermissionMode Mode { get; set; } + /// 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; - - /// 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. +/// Cancellation result for a user-requested shell command. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsGetModeResult +public sealed class CancelUserRequestedShellCommandResult { - /// Current permission mode. - [JsonPropertyName("mode")] - public PermissionMode Mode { get; set; } + /// Whether an in-flight execution was found and signalled to cancel. + [JsonPropertyName("cancelled")] + public bool Cancelled { get; set; } } -/// No parameters. +/// User-requested shell execution cancellation handle. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsGetModeRequest +internal sealed class ShellCancelUserRequestedRequest { + /// 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; } -/// Indicates whether the operation succeeded. +/// Post-compaction context window usage breakdown. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsModifyRulesResult +public sealed class HistoryCompactContextWindow { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// 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; } } -/// Scope and add/remove instructions for modifying session- or location-scoped permission rules. +/// Compaction outcome with the number of tokens and messages removed, summary text, and the resulting context window breakdown. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsModifyRulesParams +public sealed class HistoryCompactResult { - /// Rules to add to the scope. Applied before `remove`/`removeAll`. - [JsonPropertyName("add")] - public IList? Add { get; set; } + /// Post-compaction context window usage breakdown. + [JsonPropertyName("contextWindow")] + public HistoryCompactContextWindow? ContextWindow { get; set; } - /// Specific rules to remove from the scope. Ignored when `removeAll` is true. - [JsonPropertyName("remove")] - public IList? Remove { get; set; } + /// Number of messages removed during compaction. + [JsonPropertyName("messagesRemoved")] + public long MessagesRemoved { 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; } + /// Whether compaction completed successfully. + [JsonPropertyName("success")] + public bool Success { 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; } + /// Summary text produced by compaction. Omitted when compaction did not produce a summary (e.g. failure path). + [JsonPropertyName("summaryContent")] + public string? SummaryContent { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Number of tokens freed by compaction. + [JsonPropertyName("tokensRemoved")] + public long TokensRemoved { get; set; } } -/// Indicates whether the operation succeeded. +/// RPC data type for SessionHistoryCompact operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsSetRequiredResult +public sealed class SessionHistoryCompactRequest { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { 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; } + + /// 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; } } -/// Toggles whether permission prompts should be bridged into session events for this client. +/// RPC data type for SessionHistoryCompactRequestWithSession operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsSetRequiredRequest +internal sealed class SessionHistoryCompactRequestWithSession { - /// 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; } + /// 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; } /// 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; } } -/// Indicates whether the operation succeeded. +/// Number of events that were removed by the truncation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsResetSessionApprovalsResult +public sealed class HistoryTruncateResult { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// 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; } } -/// Clears session-scoped tool approvals and optionally clears location-scoped approvals and exact session-approved paths. +/// Identifier of the event to truncate to; this event and all later events are removed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsResetSessionApprovalsRequest +internal sealed class HistoryTruncateRequest { - /// Whether location-scoped approvals and exact session-approved paths are cleared too. Defaults to `true`. - [JsonPropertyName("includeLocation")] - public bool? IncludeLocation { 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; } -/// Indicates whether the operation succeeded. +/// A root user turn that the session can rewind to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsNotifyPromptShownResult +public sealed class HistoryRewindPoint { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// 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; } + + /// Whether this turn was an automatically injected autopilot continuation. + [JsonPropertyName("isAutopilotContinuation")] + public bool IsAutopilotContinuation { get; set; } + + /// Lines added by this turn's captured file changes. + [JsonPropertyName("linesAdded")] + public long LinesAdded { get; set; } + + /// Lines removed by this turn's captured file changes. + [JsonPropertyName("linesRemoved")] + public long LinesRemoved { get; set; } + + /// ISO timestamp of the user turn. + [JsonPropertyName("timestamp")] + public string Timestamp { get; set; } = string.Empty; + + /// Whether this turn itself captured any file changes. + [JsonPropertyName("turnChangedFiles")] + public bool TurnChangedFiles { get; set; } + + /// User-visible message text for the turn. + [JsonPropertyName("userMessage")] + public string UserMessage { get; set; } = string.Empty; } -/// Notification payload describing the permission prompt that the client just rendered. +/// Rewind points and file-change-tracking availability for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionPromptShownNotification +public sealed class HistoryListRewindPointsResult { - /// 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; + /// Whether this session captured file changes from its first turn. + [JsonPropertyName("fileChangeTrackingEnabled")] + public bool FileChangeTrackingEnabled { get; set; } + /// Root user turns in chronological order. Empty when `unavailableReason` is set. + [JsonPropertyName("points")] + public IList Points { get => field ??= []; 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 SessionHistoryListRewindPointsRequest +{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Snapshot of the session's recursive directory grants, exact session-approved paths, and primary working directory. +/// 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; } + + /// Lines added across the discarded turns. + [JsonPropertyName("linesAdded")] + public long LinesAdded { get; set; } + + /// Lines removed across the discarded turns. + [JsonPropertyName("linesRemoved")] + public long LinesRemoved { get; set; } + + /// Absolute path of the captured file. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; +} + +/// Files and aggregate changes for a prospective rewind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionPathsList +public sealed class HistoryPreviewRewindResult { - /// All directories currently allowed for tool access on this session. - [JsonPropertyName("directories")] - public IList Directories { get => field ??= []; 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; } - /// The primary working directory for this session. - [JsonPropertyName("primary")] - public string Primary { get; set; } = string.Empty; + /// Number of unique files in the preview. + [JsonPropertyName("fileCount")] + public long FileCount { get; set; } - /// Exact paths approved for this session without recursively allowing their descendants. - [JsonPropertyName("sessionApprovedPaths")] - public IList? SessionApprovedPaths { get; set; } + /// Files ordered by path. + [JsonPropertyName("files")] + public IList Files { 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; } } -/// No parameters; returns the session's recursive directory grants and exact session-approved paths. +/// Event boundary to preview for conversation-and-files rewind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionsPathsListRequest +internal sealed class HistoryPreviewRewindRequest { + /// ID of the user.message event that begins the discarded suffix. + [JsonPropertyName("eventId")] + public string EventId { get; set; } = string.Empty; + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the operation succeeded. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsPathsAddResult -{ - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } -} - -/// Directory path to add to the session's allowed directories. +/// A captured file that rewind intentionally left unchanged. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionPathsAddParams +public sealed class HistorySkippedFileRestore { - /// 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. + /// Absolute path of the skipped file. [JsonPropertyName("path")] public string Path { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Reason the file was not restored. + [JsonPropertyName("reason")] + public HistoryFileRestoreSkipReason Reason { get; set; } } -/// Indicates whether the operation succeeded. +/// Structured outcome of a rewind request. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsPathsUpdatePrimaryResult +public sealed class HistoryRewindResult { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { 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; } + + /// 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; } + + /// 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 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; } + + /// 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; } } -/// Directory path to set as the session's new primary working directory. +/// Boundary and mode for rewinding session history. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionPathsUpdatePrimaryParams +internal sealed class HistoryRewindRequest { - /// Directory to set as the new primary working directory for the session's permission policy. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// 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 the supplied path is within the session's allowed directories. +/// Indicates whether an in-progress background compaction was cancelled. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionPathsAllowedCheckResult +public sealed class HistoryCancelBackgroundCompactionResult { - /// Whether the path is within the session's allowed directories. - [JsonPropertyName("allowed")] - public bool Allowed { get; set; } + /// 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; } } -/// Path to evaluate against the session's allowed directories. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionPathsAllowedCheckParams +internal sealed class SessionHistoryCancelBackgroundCompactionRequest { - /// Path to check against the session's allowed directories. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Indicates whether the supplied path is within the session's workspace directory. +/// Indicates whether an in-progress manual compaction was aborted. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionPathsWorkspaceCheckResult +public sealed class HistoryAbortManualCompactionResult { - /// Whether the path is within the session workspace directory. - [JsonPropertyName("allowed")] - public bool Allowed { get; set; } + /// 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; } } -/// Path to evaluate against the session's workspace (primary) directory. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionPathsWorkspaceCheckParams +internal sealed class SessionHistoryAbortManualCompactionRequest { - /// Path to check against the session workspace directory. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; - /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Resolved location-permissions key and type. +/// Markdown summary of the conversation context (empty when not available). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionLocationResolveResult +public sealed class HistorySummarizeForHandoffResult { - /// 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; } + /// 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; } -/// Working directory to resolve into a location-permissions key. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionLocationResolveParams +internal sealed class SessionHistorySummarizeForHandoffRequest { /// 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; } -/// Summary of persisted location permissions applied to the session. +/// 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 PermissionLocationApplyResult +public sealed class HistoryClearContextResult { - /// 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; } + /// 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; } } -/// Working directory to load persisted location permissions for. +/// Parameters for clearing the conversation and seeding the window that replaces it. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionLocationApplyParams +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; - - /// Working directory whose persisted location permissions should be applied. - [JsonPropertyName("workingDirectory")] - public string WorkingDirectory { get; set; } = string.Empty; } -/// Indicates whether the operation succeeded. +/// 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 PermissionsLocationsAddToolApprovalResult +public sealed class QueuePendingItems { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { 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; } -/// Tool approval to persist and apply. -/// 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 -{ - /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; -} + /// Human-readable text to display for this queue entry in the UI. + [JsonPropertyName("displayText")] + public string DisplayText { get; set; } = string.Empty; + /// Stable opaque id for the canonical queued item. Batch rows share one id. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; -/// Location-persisted tool approval details for specific command identifiers. -/// The commands variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsCommands : PermissionsLocationsAddToolApprovalDetails -{ - /// - [JsonIgnore] - public override string Kind => "commands"; + /// Whether this item is a queued user message or a queued slash command / model change. + [JsonPropertyName("kind")] + public QueuePendingItemsKind Kind { get; set; } - /// Command identifiers covered by this approval. - [JsonPropertyName("commandIdentifiers")] - public required IList CommandIdentifiers { 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; } + + /// 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; } } -/// Location-persisted tool approval details for read-only filesystem operations. -/// The read variant of . +/// Snapshot of the session's pending queued items and immediate-steering messages. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsRead : PermissionsLocationsAddToolApprovalDetails +public sealed class QueuePendingItemsResult { - /// - [JsonIgnore] - public override string Kind => "read"; + /// 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; } } -/// Location-persisted tool approval details for filesystem write operations. -/// The write variant of . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsWrite : PermissionsLocationsAddToolApprovalDetails +internal sealed class SessionQueuePendingItemsRequest { - /// - [JsonIgnore] - public override string Kind => "write"; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Location-persisted tool approval details for an MCP server tool, or all tools when `toolName` is null. -/// The mcp variant of . +/// Internal snapshot of native queue state for local session orchestration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsMcp : PermissionsLocationsAddToolApprovalDetails +internal sealed class QueueSnapshotResult { - /// - [JsonIgnore] - public override string Kind => "mcp"; + /// Queue item identifier of a model switch that has been dequeued but not yet applied. + [JsonPropertyName("inFlightModelChangeId")] + public string? InFlightModelChangeId { get; set; } - /// MCP server name. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// Insertion orders for queued items, aligned with `items`. + [JsonPropertyName("itemOrders")] + public IList? ItemOrders { get; set; } - /// MCP tool name, or null to cover every tool on the server. - [JsonPropertyName("toolName")] - public string? ToolName { get; set; } -} + /// User-facing pending items in FIFO order. + [JsonPropertyName("items")] + public IList Items { get => field ??= []; set; } -/// Location-persisted tool 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 -{ - /// - [JsonIgnore] - public override string Kind => "mcp-sampling"; + /// Insertion orders for immediate steering messages, aligned with `steeringMessages`. + [JsonPropertyName("steeringMessageOrders")] + public IList? SteeringMessageOrders { get; set; } - /// MCP server name. - [JsonPropertyName("serverName")] - public required string ServerName { get; set; } + /// Immediate steering messages waiting for an active turn. + [JsonPropertyName("steeringMessages")] + public IList SteeringMessages { get => field ??= []; set; } } -/// Location-persisted tool approval details for writes to long-term memory. -/// The memory variant of . +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsMemory : PermissionsLocationsAddToolApprovalDetails +internal sealed class SessionQueueSnapshotRequest { - /// - [JsonIgnore] - public override string Kind => "memory"; + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Location-persisted tool approval details for a custom tool, keyed by tool name. -/// The custom-tool variant of . +/// Result of moving a queued item. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsCustomTool : PermissionsLocationsAddToolApprovalDetails +public sealed class QueueMoveItemResult { - /// - [JsonIgnore] - public override string Kind => "custom-tool"; - - /// Custom tool name. - [JsonPropertyName("toolName")] - public required string ToolName { get; set; } + /// True when the item changed position; false when it was already at the requested position. + [JsonPropertyName("changed")] + public bool Changed { get; set; } } -/// Location-persisted tool approval details for extension-management operations, optionally narrowed by operation. -/// The extension-management variant of . +/// Parameters for moving a queued item by stable id. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsExtensionManagement : PermissionsLocationsAddToolApprovalDetails +internal sealed class QueueMoveItemRequest { - /// - [JsonIgnore] - public override string Kind => "extension-management"; + /// Stable opaque queued-item id. + [JsonPropertyName("id")] + public string Id { 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; } + /// 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; } } -/// Location-persisted workflow approval, optionally narrowed by approval key. -/// The workflow variant of . +/// Result of inserting a queued message. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsWorkflow : PermissionsLocationsAddToolApprovalDetails +public sealed class QueueInsertAtResult { - /// - [JsonIgnore] - public override string Kind => "workflow"; - - /// 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; } + /// Fresh stable opaque id assigned to the inserted item. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; } -/// Location-persisted tool approval details for an extension's permission-gated capability access, keyed by extension name. -/// The extension-permission-access variant of . +/// Serializable message fields accepted by queue.insertAt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsExtensionPermissionAccess : PermissionsLocationsAddToolApprovalDetails +public sealed class QueueInsertMessage { - /// - [JsonIgnore] - public override string Kind => "extension-permission-access"; + /// Optional explicit agent mode. When omitted, the session's current mode is assigned. + [JsonPropertyName("agentMode")] + public SendAgentMode? AgentMode { get; set; } - /// Extension name. - [JsonPropertyName("extensionName")] - public required string ExtensionName { get; set; } -} + /// Optional attachments for the message. + [JsonPropertyName("attachments")] + public IList? Attachments { 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 . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class PermissionsLocationsAddToolApprovalDetailsExtensionEnvAccess : PermissionsLocationsAddToolApprovalDetails -{ - /// - [JsonIgnore] - public override string Kind => "extension-env-access"; + /// Whether the message is billable. + [JsonPropertyName("billable")] + public bool? Billable { get; set; } - /// Names of the sensitive environment variables this approval covers. Values are never persisted. - [JsonPropertyName("environmentVariables")] - public required IList EnvironmentVariables { get; set; } + /// Accepted for internal SendOptions compatibility but ignored; delivery is derived from current session activity. + [JsonPropertyName("delivery")] + public string? Delivery { get; set; } - /// Extension name. - [JsonPropertyName("extensionName")] - public required string ExtensionName { get; set; } -} + /// Optional user-facing display text. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { get; set; } -/// Location-scoped tool approval to persist. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionLocationAddToolApprovalParams -{ - /// Tool approval to persist and apply. - [JsonPropertyName("approval")] - public PermissionsLocationsAddToolApprovalDetails Approval { get => field ??= new(); set; } + /// Accepted for SendOptions compatibility but ignored; inserted items always use queued delivery semantics. + [JsonPropertyName("mode")] + public SendMode? Mode { get; set; } - /// Location key (git root or cwd) to persist the approval to. - [JsonPropertyName("locationKey")] - public string LocationKey { get; set; } = string.Empty; + /// Accepted for SendOptions compatibility but ignored; the requested public position controls placement. + [JsonPropertyName("prepend")] + public bool? Prepend { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// The user message text. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; -/// Folder trust check result. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class FolderTrustCheckResult -{ - /// Whether the folder is trusted. - [JsonPropertyName("trusted")] - public bool Trusted { get; set; } -} + /// Per-turn request headers. + [JsonPropertyName("requestHeaders")] + public IDictionary? RequestHeaders { get; set; } -/// Folder path to check for trust. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FolderTrustCheckParams -{ - /// Folder path to check. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Required tool name for the turn, when any. + [JsonPropertyName("requiredTool")] + public string? RequiredTool { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// 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; } -/// Indicates whether the operation succeeded. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsFolderTrustAddTrustedResult -{ - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Accepted for SendOptions compatibility but ignored; insertion scheduling is controlled by the queue drain state. + [JsonPropertyName("wait")] + public bool? Wait { get; set; } } -/// Folder path to add to trusted folders. +/// Parameters for inserting a queued message at a public visible position. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class FolderTrustAddParams +internal sealed class QueueInsertAtRequest { - /// Folder path to mark as trusted. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Queued message contents and delivery metadata. + [JsonPropertyName("message")] + public QueueInsertMessage Message { get => field ??= new(); 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; } -/// Indicates whether the operation succeeded. +/// Result of removing a queued item. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class PermissionsUrlsSetUnrestrictedModeResult +public sealed class QueueRemoveAtResult { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// True when the addressed item was removed. + [JsonPropertyName("removed")] + public bool Removed { get; set; } } -/// Whether the URL-permission policy should run in unrestricted mode. +/// Parameters for removing a queued item by stable id. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class PermissionUrlsSetUnrestrictedModeParams +internal sealed class QueueRemoveAtRequest { - /// 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; } + /// 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; } -/// The repository the remote session targets. +/// Result of editing a queued message. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataSnapshotRemoteMetadataRepository +public sealed class QueueUpdateTextResult { - /// The branch the remote session is operating on. - [JsonPropertyName("branch")] - public string Branch { get; set; } = string.Empty; - - /// The GitHub repository name (without owner). - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - /// The GitHub owner (user or organization) of the target repository. - [JsonPropertyName("owner")] - public string Owner { get; set; } = string.Empty; + /// True when the stored text changed. + [JsonPropertyName("updated")] + public bool Updated { get; set; } } -/// Remote-session-specific metadata. Populated only when `isRemote` is true. Fields are immutable for the lifetime of the session. +/// Parameters for editing a single queued message. [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; } - - /// The repository the remote session targets. - [JsonPropertyName("repository")] - public MetadataSnapshotRemoteMetadataRepository Repository { get => field ??= new(); 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; } - - /// Whether the remote task originated from Copilot Coding Agent (cca) or a CLI `--remote` invocation. - [JsonPropertyName("taskType")] - public MetadataSnapshotRemoteMetadataTaskType? TaskType { get; set; } -} - -/// Public-facing projection of workspace metadata for SDK / TUI consumers. -public sealed class SessionMetadataSnapshotWorkspace +internal sealed class QueueUpdateTextRequest { - /// Branch checked out at session start, if any. - [JsonPropertyName("branch")] - public string? Branch { get; set; } - - /// ISO 8601 timestamp when the workspace was created. - [JsonPropertyName("created_at")] - public DateTimeOffset? CreatedAt { get; set; } - - /// Current working directory at session start. - [JsonPropertyName("cwd")] - public string? Cwd { get; set; } - - /// Resolved git root for cwd, if any. - [JsonPropertyName("git_root")] - public string? GitRoot { get; set; } - - /// Repository host type, if known. - [JsonPropertyName("host_type")] - public WorkspaceSummaryHostType? HostType { get; set; } + /// Optional replacement prompt displayed to the user. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { 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)] + /// Stable opaque ID of the queued item to edit. [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; - /// Display name for the session, if set. - [JsonPropertyName("name")] - public string? Name { get; set; } + /// Replacement prompt sent to the model. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; - /// Repository identifier in 'owner/repo' or 'org/project/repo' format, if any. - [JsonPropertyName("repository")] - public string? Repository { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// ISO 8601 timestamp when the workspace was last updated. - [JsonPropertyName("updated_at")] - public DateTimeOffset? UpdatedAt { get; set; } +/// Result of withdrawing a user message. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class QueueWithdrawMessageResult +{ + /// 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; } - /// Whether the display name was explicitly set by the user. - [JsonPropertyName("user_named")] - public bool? UserNamed { get; set; } + /// True when the message left the queue or, for a running turn, history. + [JsonPropertyName("removed")] + public bool Removed { get; set; } } -/// Point-in-time snapshot of slow-changing session identifier and state fields. +/// 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 SessionMetadataSnapshot +internal sealed class QueueWithdrawMessageRequest { - /// 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; } + /// 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; - /// Runtime client name associated with the session (telemetry identifier). - [JsonPropertyName("clientName")] - public string? ClientName { get; set; } + /// Message identity returned by send, not the queue item id. Batch messages are not eligible. + [JsonPropertyName("messageId")] + public string MessageId { get; set; } = string.Empty; - /// The current agent mode for this session (e.g., 'interactive', 'plan', 'autopilot'). - [JsonPropertyName("currentMode")] - public MetadataSnapshotCurrentMode CurrentMode { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// 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; } +/// Append to one pending steering message without changing its identity or delivery position. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class QueueAppendSteeringRequest +{ + /// Mode captured at submission. Only steering messages in the same mode may be combined. + [JsonPropertyName("agentMode")] + public SendAgentMode AgentMode { get; set; } - /// User-provided name supplied at session construction (via `--name`), if any. Immutable after construction. - [JsonPropertyName("initialName")] - public string? InitialName { 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; } - /// 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; } + /// Display text to append to the existing preview after a blank line. + [JsonPropertyName("displayPrompt")] + public string DisplayPrompt { get; set; } = string.Empty; - /// 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; } + /// 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; - /// 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; } + /// 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; - /// Currently selected model identifier, if any. - [JsonPropertyName("selectedModel")] - public string? SelectedModel { get; set; } + /// Text to append after a blank line. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; - /// The unique identifier of the session. + /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - - /// Current session limits, or null when no limits are active. - [JsonPropertyName("sessionLimits")] - public SessionLimitsConfig? SessionLimits { get; set; } - - /// ISO 8601 timestamp of when the session started. - [JsonPropertyName("startTime")] - public DateTimeOffset StartTime { 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; } - - /// Absolute path to the session's current working directory. - [JsonPropertyName("workingDirectory")] - public string WorkingDirectory { get; set; } = string.Empty; - - /// 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; } - - /// 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; } } -/// Identifies the target session. +/// Result of duplicating a queued item. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMetadataSnapshotRequest +public sealed class QueueDuplicateAtResult { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Fresh stable opaque id assigned to the duplicate. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; } -/// Identifies the target session. +/// Parameters for duplicating a queued item. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMetadataGetClientMetadataRequest +internal sealed class QueueDuplicateAtRequest { + /// 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; } -/// 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. +/// 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)] -internal sealed class MetadataUpdateClientMetadataRequest +internal sealed class QueueSetDrainPausedRequest { - /// 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; } + /// Whether queued-lane draining should be paused. + [JsonPropertyName("paused")] + public bool Paused { 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 the local session is currently processing a turn or background continuation. +/// Result of trying to steer a queued message into a live turn. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataIsProcessingResult +public sealed class QueueSendNowResult { - /// 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; } + /// True when the item was accepted into the steering lane; false when no main turn was live. + [JsonPropertyName("steered")] + public bool Steered { get; set; } } -/// Identifies the target session. +/// Parameters for steering a queued message into a live turn. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMetadataIsProcessingRequest +internal sealed class QueueSendNowRequest { + /// 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; } -/// Current activity flags for the session. +/// Whether the native queue has pending work. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionActivity +internal sealed class QueueHasPendingResult { - /// 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; } + /// True when queued or immediate native work is pending. + [JsonPropertyName("hasPending")] + public bool HasPending { get; set; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMetadataActivityRequest +internal sealed class SessionQueueHasPendingRequest { /// 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 -{ - /// 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; } - - /// Tokens consumed by the system prompt. - [JsonPropertyName("systemTokens")] - public long SystemTokens { 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; } -} - -/// Token breakdown for the session's current context window, or null if uninitialized. +/// Whether a deferred-idle drain should run. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataContextInfoResult +internal sealed class QueueBeginDeferredIdleDrainResult { - /// 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; } + /// True when the host should run finishDeferredIdleDrain asynchronously. + [JsonPropertyName("shouldDrain")] + public bool ShouldDrain { get; set; } } -/// Model identifier and token limits used to compute the context-info breakdown. +/// Inputs for starting a deferred-idle drain. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class MetadataContextInfoRequest +internal sealed class QueueBeginDeferredIdleDrainRequest { - /// 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; } - - /// 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; } + /// 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; } -/// 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 +/// Action selected by the native deferred-idle drain. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class QueueFinishDeferredIdleDrainResult { - /// 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; } -} + /// Whether the deferred idle was caused by an aborted foreground turn. + [JsonPropertyName("aborted")] + public bool Aborted { get; set; } -/// Successful compaction history for the session. -public sealed class MetadataContextAttributionResultContextAttributionCompactions -{ - /// Number of successful compactions in this session. - [JsonPropertyName("count")] - public long Count { get; set; } + /// One of none, processQueue, or emitSessionIdle. + [JsonPropertyName("action")] + public string Action { get; set; } = string.Empty; } -/// RPC data type for MetadataContextAttributionResultContextAttributionEntry operations. -public sealed class MetadataContextAttributionResultContextAttributionEntry +/// Inputs for completing a deferred-idle drain. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class QueueFinishDeferredIdleDrainRequest { - /// 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; + /// Whether the host still has active background work. + [JsonPropertyName("activeBackgroundWork")] + public bool ActiveBackgroundWork { 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; } + /// Whether native queued work remains. + [JsonPropertyName("hasPending")] + public bool HasPending { get; set; } - /// Token count currently in context attributable to this entry. - [JsonPropertyName("tokens")] - public long Tokens { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Per-source token attribution snapshot for the current context window. The heaviest individual messages are available separately via `metadata.getContextHeaviestMessages`. -public sealed class MetadataContextAttributionResultContextAttribution +/// Inputs for marking session.idle deferred in native state. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class QueueDeferSessionIdleRequest { - /// 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; } - - /// 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; - - /// 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; - - /// 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 deferred idle was caused by an aborted foreground turn. + [JsonPropertyName("aborted")] + public bool Aborted { 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 a user-facing pending item was removed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataContextAttributionResult +public sealed class QueueRemoveMostRecentResult { - /// 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; } + /// 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; } } /// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionMetadataGetContextAttributionRequest +internal sealed class SessionQueueRemoveMostRecentRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// A single large message currently in context. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ContextHeaviestMessage +internal sealed class SessionQueueClearRequest { - /// 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; - - /// 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; } + /// 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. +/// Internal filter for consuming queued system notifications. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataContextHeaviestMessagesResult +internal sealed class QueueConsumeSystemNotificationsRequest { - /// Heaviest messages, most-expensive first. - [JsonPropertyName("messages")] - public IList Messages { get => field ??= []; set; } + /// Opaque runtime-owned filter object. + [JsonPropertyName("filter")] + public JsonElement Filter { 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Parameters for the heaviest-messages query. +/// Result of enqueueing the resume-pending wake item. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class MetadataContextHeaviestMessagesRequest +internal sealed class QueueEnqueueResumePendingResult { - /// Maximum number of messages to return, most-expensive first. Omit for the server default. - [JsonPropertyName("limit")] - public long? Limit { get; set; } + /// True when a wake item was newly queued. + [JsonPropertyName("queued")] + public bool Queued { get; set; } +} +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionQueueEnqueueResumePendingRequest +{ /// 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. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataRecordContextChangeResult +internal sealed class SessionQueueProcessRequest { + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. +/// Cursor, batch size, and optional long-poll/filter parameters for reading session events. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionWorkingDirectoryContext +internal sealed class EventLogReadRequest { - /// Merge-base commit SHA (fork point from the remote default branch). - [JsonPropertyName("baseCommit")] - public string? BaseCommit { 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; } - /// Current git branch name. - [JsonPropertyName("branch")] - public string? Branch { 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; } - /// Current working directory path. - [JsonPropertyName("cwd")] - public string Cwd { get; set; } = string.Empty; + /// 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; } - /// Root directory of the git repository, resolved via git rev-parse. - [JsonPropertyName("gitRoot")] - public string? GitRoot { 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; } - /// Head commit of the current git branch. - [JsonPropertyName("headCommit")] - public string? HeadCommit { get; set; } + /// 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; } - /// Hosting platform type of the repository. - [JsonPropertyName("hostType")] - public SessionWorkingDirectoryContextHostType? HostType { get; set; } + /// Maximum number of events to return in this batch (1–1000, default 200). + [JsonPropertyName("max")] + public long? Max { 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { 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; } + /// 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; } } -/// Updated working-directory/git context to record on the session. +/// 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)] -internal sealed class MetadataRecordContextChangeRequest +public sealed class EventLogTailResult { - /// Updated working directory and git context. Emitted as the new payload of `session.context_changed`. - [JsonPropertyName("context")] - public SessionWorkingDirectoryContext Context { get => field ??= new(); set; } + /// 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; +} +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionEventLogTailRequest +{ /// 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. +/// Opaque handle representing an event-type interest registration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class MetadataSetWorkingDirectoryResult +public sealed class RegisterEventInterestResult { - /// Working directory after the update. - [JsonPropertyName("workingDirectory")] - public string WorkingDirectory { get; set; } = string.Empty; + /// 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; } -/// 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. +/// Event type to register consumer interest for, used by runtime gating logic. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class MetadataSetWorkingDirectoryRequest +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; - - /// 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 EventLogReleaseInterestResult { - /// 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. +/// Opaque handle previously returned by `registerInterest` to release. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class MetadataRecomputeContextTokensRequest +internal sealed class ReleaseEventInterestParams { - /// 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; + /// 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; } -/// Availability of built-in job tools surfaced to boundary consumers. +/// Request count and cost metrics for this model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSettingsBuiltInToolAvailabilitySnapshot +public sealed class UsageMetricsModelMetricRequests { - /// Whether the create-pull-request tool is available. - [JsonPropertyName("createPullRequest")] - public bool? CreatePullRequest { get; set; } + /// User-initiated premium request cost (with multiplier applied). + [JsonPropertyName("cost")] + public double Cost { get; set; } - /// Whether the report-progress tool is available. - [JsonPropertyName("reportProgress")] - public bool? ReportProgress { get; set; } + /// Number of API requests made with this model. + [JsonPropertyName("count")] + public long Count { get; set; } } -/// Redacted job settings for a session. The job nonce is excluded. +/// Per-model token-detail entry containing the accumulated token count for one token type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSettingsJobSnapshot +public sealed class UsageMetricsModelMetricTokenDetail { - /// Availability of job-specific built-in tools. - [JsonPropertyName("builtInToolAvailability")] - public SessionSettingsBuiltInToolAvailabilitySnapshot? BuiltInToolAvailability { get; set; } + /// Accumulated token count for this token type. + [JsonPropertyName("tokenCount")] + public long TokenCount { get; set; } +} - /// GitHub Actions event type for the job. - [JsonPropertyName("eventType")] - public string? EventType { get; set; } +/// Token usage metrics for this model. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class UsageMetricsModelMetricUsage +{ + /// Total tokens read from prompt cache. + [JsonPropertyName("cacheReadTokens")] + public long CacheReadTokens { get; set; } - /// Whether this is the workflow's trigger job. - [JsonPropertyName("isTriggerJob")] - public bool? IsTriggerJob { 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; } } -/// Redacted model routing settings for a session. +/// 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 SessionSettingsModelSnapshot +public sealed class UsageMetricsModelMetric { - /// Agent service callback URL for job and progress updates. - [JsonPropertyName("callbackUrl")] - public string? CallbackUrl { 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; } - /// Default reasoning effort for the selected model. - [JsonPropertyName("defaultReasoningEffort")] - public string? DefaultReasoningEffort { get; set; } + /// Request count and cost metrics for this model. + [JsonPropertyName("requests")] + public UsageMetricsModelMetricRequests Requests { get => field ??= new(); set; } - /// Agent job identifier for the session. - [JsonPropertyName("instanceId")] - public string? InstanceId { get; set; } + /// Token count details per type. + [JsonPropertyName("tokenDetails")] + public IDictionary? TokenDetails { get; set; } - /// Selected model identifier. - [JsonPropertyName("model")] - public string? Model { 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; } } -/// Online-evaluation settings safe to expose across the SDK boundary. +/// 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 SessionSettingsOnlineEvaluationSnapshot +public sealed class UsageMetricsAgentMetric { - /// Whether online evaluation is disabled. - [JsonPropertyName("disableOnlineEvaluation")] - public bool? DisableOnlineEvaluation { 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; } - /// Whether online-evaluation output-file generation is enabled. - [JsonPropertyName("enableOnlineEvaluationOutputFile")] - public bool? EnableOnlineEvaluationOutputFile { get; set; } + /// Configured agent name, when this is a subagent. + [JsonPropertyName("agentName")] + public string? AgentName { get; set; } + + /// Per-model usage for this agent, keyed by model identifier. + [JsonPropertyName("modelMetrics")] + public IDictionary ModelMetrics { get => field ??= new Dictionary(); set; } + + /// Time spent in model API calls by this agent, in milliseconds. + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonPropertyName("totalApiDurationMs")] + public TimeSpan TotalApiDuration { get; set; } + + /// Accumulated nano-AI units cost for this agent. + [JsonPropertyName("totalNanoAiu")] + public double TotalNanoAiu { get; set; } } -/// Redacted repository and GitHub host settings for a session. +/// Aggregated code change metrics. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSettingsRepoSnapshot +public sealed class UsageMetricsCodeChanges { - /// Checked-out repository branch. - [JsonPropertyName("branch")] - public string? Branch { get; set; } + /// Distinct file paths modified during the session. + [JsonPropertyName("filesModified")] + public IList FilesModified { get => field ??= []; set; } - /// Checked-out commit SHA. - [JsonPropertyName("commit")] - public string? Commit { get; set; } + /// Number of distinct files modified. + [JsonPropertyName("filesModifiedCount")] + public long FilesModifiedCount { get; set; } - /// GitHub server host name. - [JsonPropertyName("host")] - public string? Host { get; set; } + /// Total lines of code added. + [JsonPropertyName("linesAdded")] + public long LinesAdded { get; set; } - /// Protocol used to access the GitHub host. - [JsonPropertyName("hostProtocol")] - public string? HostProtocol { get; set; } + /// Total lines of code removed. + [JsonPropertyName("linesRemoved")] + public long LinesRemoved { get; set; } +} - /// GitHub repository database ID. - [JsonPropertyName("id")] - public double? Id { get; set; } +/// 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; } +} - /// Repository name. - [JsonPropertyName("name")] - public string? Name { get; set; } +/// 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 UsageGetMetricsResult +{ + /// Per-agent usage metrics, keyed by agent instance identifier. The main conversation uses the stable key `main`. + [JsonPropertyName("agentMetrics")] + public IDictionary? AgentMetrics { get; set; } - /// GitHub repository owner database ID. - [JsonPropertyName("ownerId")] - public double? OwnerId { get; set; } + /// Aggregated code change metrics. + [JsonPropertyName("codeChanges")] + public UsageMetricsCodeChanges CodeChanges { get => field ??= new(); set; } - /// Repository owner login. - [JsonPropertyName("ownerName")] - public string? OwnerName { get; set; } + /// Currently active model identifier. + [JsonPropertyName("currentModel")] + public string? CurrentModel { get; set; } - /// Number of commits in the pull request. - [JsonPropertyName("prCommitCount")] - public double? PrCommitCount { get; set; } + /// Input tokens from the most recent main-agent API call. + [JsonPropertyName("lastCallInputTokens")] + public long LastCallInputTokens { get; set; } - /// Whether the repository is writable. - [JsonPropertyName("readWrite")] - public bool? ReadWrite { get; set; } + /// Output tokens from the most recent main-agent API call. + [JsonPropertyName("lastCallOutputTokens")] + public long LastCallOutputTokens { get; set; } - /// GitHub secret-scanning service URL. - [JsonPropertyName("secretScanningUrl")] - public string? SecretScanningUrl { get; set; } + /// Per-model token and request metrics, keyed by model identifier. + [JsonPropertyName("modelMetrics")] + public IDictionary ModelMetrics { get => field ??= new Dictionary(); set; } - /// GitHub server base URL. - [JsonPropertyName("serverUrl")] - public string? ServerUrl { get; set; } -} + /// ISO 8601 timestamp when the session started. + [JsonPropertyName("sessionStartTime")] + public DateTimeOffset SessionStartTime { get; set; } -/// Redacted validation and memory-tool settings for a session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionSettingsValidationSnapshot -{ - /// Whether advisory validation is enabled. - [JsonPropertyName("advisoryEnabled")] - public bool? AdvisoryEnabled { get; set; } + /// Session-wide per-token-type accumulated token counts. + [JsonPropertyName("tokenDetails")] + public IDictionary? TokenDetails { get; set; } - /// Whether CodeQL validation is enabled. - [JsonPropertyName("codeqlEnabled")] - public bool? CodeqlEnabled { get; set; } + /// Total time spent in model API calls (milliseconds). + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonPropertyName("totalApiDurationMs")] + public TimeSpan TotalApiDuration { get; set; } - /// Whether code-review validation is enabled. - [JsonPropertyName("codeReviewEnabled")] - public bool? CodeReviewEnabled { get; set; } + /// Session-wide accumulated nano-AI units cost. + [JsonPropertyName("totalNanoAiu")] + public double? TotalNanoAiu { get; set; } - /// Model used for code-review validation. - [JsonPropertyName("codeReviewModel")] - public string? CodeReviewModel { get; set; } + /// Total user-initiated premium request cost across all models (may be fractional due to multipliers). + [JsonPropertyName("totalPremiumRequestCost")] + public double TotalPremiumRequestCost { get; set; } - /// Dependabot validation timeout budget in seconds. - [JsonPropertyName("dependabotTimeout")] - public double? DependabotTimeout { get; set; } + /// Raw count of user-initiated API requests. + [JsonPropertyName("totalUserRequests")] + public long TotalUserRequests { get; set; } +} - /// Whether the memory-store tool is enabled. - [JsonPropertyName("memoryStoreEnabled")] - public bool? MemoryStoreEnabled { get; set; } +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionUsageGetMetricsRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} - /// Whether the memory-vote tool is enabled. - [JsonPropertyName("memoryVoteEnabled")] - public bool? MemoryVoteEnabled { get; set; } +/// 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; +} - /// 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; } +/// Baseline data provenance for a prediction. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionLimitPredictionBaselineData +{ + /// End of the baseline data slice. + [JsonPropertyName("windowEnd")] + public string WindowEnd { get; set; } = string.Empty; + + /// Start of the baseline data slice. + [JsonPropertyName("windowStart")] + public string WindowStart { get; set; } = string.Empty; } -/// Redacted, serializable view of session runtime settings for SDK boundary consumers. Secrets and raw feature flags are intentionally excluded. +/// Semantic usage tier and its AI-credit cap. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSettingsSnapshot +public sealed class SessionLimitPredictionTierOption { - /// Name of the SDK client that created the session. - [JsonPropertyName("clientName")] - public string? ClientName { get; set; } + /// AI-credit cap for this tier. + [JsonPropertyName("cap")] + public double Cap { get; set; } - /// Redacted job settings. - [JsonPropertyName("job")] - public SessionSettingsJobSnapshot Job { get => field ??= new(); set; } + /// Semantic usage tier. + [JsonPropertyName("tier")] + public SessionLimitPredictionTier Tier { get; set; } +} - /// Redacted model routing settings. - [JsonPropertyName("model")] - public SessionSettingsModelSnapshot Model { get => field ??= new(); set; } +/// Explainable AI-credit session-limit prediction. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class SessionLimitPredictionDetails +{ + /// Baseline data provenance. + [JsonPropertyName("baselineData")] + public SessionLimitPredictionBaselineData BaselineData { get => field ??= new(); set; } - /// Online-evaluation settings safe for SDK consumers. - [JsonPropertyName("onlineEvaluation")] - public SessionSettingsOnlineEvaluationSnapshot OnlineEvaluation { get => field ??= new(); set; } + /// Client population used for the prediction. + [JsonPropertyName("clientType")] + public SessionLimitPredictionClientType ClientType { get; set; } - /// Redacted repository and host settings. - [JsonPropertyName("repo")] - public SessionSettingsRepoSnapshot Repo { get => field ??= new(); set; } + /// Resolved model family when known. + [JsonPropertyName("family")] + public string? Family { get; set; } - /// Session start time as Unix epoch milliseconds. - [JsonPropertyName("startTimeMs")] - public double? StartTimeMs { get; set; } + /// Model identifier used for lookup. + [JsonPropertyName("modelId")] + public string ModelId { get; set; } = string.Empty; - /// Session timeout in milliseconds. - [JsonPropertyName("timeoutMs")] - public double? TimeoutMs { get; set; } + /// Recommended maximum AI credits for this session. + [JsonPropertyName("recommendedCap")] + public double RecommendedCap { get; set; } - /// Redacted validation and memory-tool settings. - [JsonPropertyName("validation")] - public SessionSettingsValidationSnapshot Validation { get => field ??= new(); set; } + /// Tier chosen as the recommended cap. + [JsonPropertyName("recommendedTier")] + public SessionLimitPredictionTier RecommendedTier { get; 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; } + /// Baseline fallback level used to create the prediction. + [JsonPropertyName("source")] + public SessionLimitPredictionSource Source { 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; + + /// Ordered usage tiers and their AI-credit caps. + [JsonPropertyName("tiers")] + public IList Tiers { get => field ??= []; set; } } -/// Identifies the target session. +/// The available variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSettingsSnapshotRequest +public partial class SessionLimitPredictionResultAvailable : SessionLimitPredictionResult { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + [JsonIgnore] + public override string Kind => "available"; + + /// Predicted session limit details. + [JsonPropertyName("prediction")] + public required SessionLimitPredictionDetails Prediction { get; set; } } -/// Result of evaluating a Rust-owned settings predicate. +/// The unavailable variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSettingsEvaluatePredicateResult +public partial class SessionLimitPredictionResultUnavailable : SessionLimitPredictionResult { - /// Whether the named settings predicate evaluated to enabled. - [JsonPropertyName("enabled")] - public bool Enabled { get; set; } + /// + [JsonIgnore] + public override string Kind => "unavailable"; + + /// Reason no prediction is available. + [JsonPropertyName("reason")] + public required SessionLimitPredictionUnavailableReason Reason { get; set; } } -/// Named Rust-owned settings predicate to evaluate for this session. +/// RPC data type for SessionLimitPredictionPredict operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionSettingsEvaluatePredicateRequest +public sealed class SessionLimitPredictionPredictRequest { - /// 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; + /// Client type to size for. Defaults to `cli-interactive`. + [JsonPropertyName("clientType")] + public SessionLimitPredictionClientType? ClientType { get; set; } - /// Tool name for tool-scoped predicates such as trivial-change handling. - [JsonPropertyName("toolName")] - public string? ToolName { get; set; } + /// Optional model identifier override. If omitted, the session's current model is used. + [JsonPropertyName("modelId")] + public string? ModelId { get; set; } } -/// Content-exclusion decision for one requested path. +/// RPC data type for SessionLimitPredictionPredictRequestWithSession operations. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ContentExclusionPathCheck +internal sealed class SessionLimitPredictionPredictRequestWithSession { - /// Whether the session's complete content-exclusion policy excludes the path. - [JsonPropertyName("excluded")] - public bool Excluded { get; set; } + /// Client type to size for. Defaults to `cli-interactive`. + [JsonPropertyName("clientType")] + public SessionLimitPredictionClientType? ClientType { get; set; } - /// The path supplied by the caller. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Optional model identifier override. If omitted, the session's current model is used. + [JsonPropertyName("modelId")] + public string? ModelId { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Batch content-exclusion result. Callers must fail closed when policy evaluation is unavailable. +/// GitHub URL for the session and a flag indicating whether remote steering is enabled. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ContentExclusionCheckPathsResult +public sealed class RemoteEnableResult { - /// 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; } + /// Whether remote steering is enabled. + [JsonPropertyName("remoteSteerable")] + public bool RemoteSteerable { get; set; } - /// Per-path decisions in request order. Empty when available is false. - [JsonPropertyName("checks")] - public IList Checks { get => field ??= []; set; } + /// GitHub frontend URL for this session. + [Url] + [StringSyntax(StringSyntaxAttribute.Uri)] + [JsonPropertyName("url")] + public string? Url { get; set; } } -/// Local file system absolute paths within the session working directory to check against its content-exclusion policy. +/// 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 ContentExclusionCheckPathsRequest +internal sealed class RemoteEnableRequest { - /// 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; } + /// 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; } /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Identifier of the spawned process, used to correlate streamed output and exit notifications. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ShellExecResult -{ - /// Unique identifier for tracking streamed output. - [JsonPropertyName("processId")] - public string ProcessId { get; set; } = string.Empty; -} - -/// Shell command to run, with optional working directory and timeout in milliseconds. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ShellExecRequest +internal sealed class SessionRemoteDisableRequest { - /// 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; - - /// Timeout in milliseconds (default: 30000). - [JsonConverter(typeof(MillisecondsTimeSpanConverter))] - [JsonPropertyName("timeout")] - public TimeSpan? Timeout { get; set; } } -/// Indicates whether the signal was delivered; false if the process was unknown or already exited. +/// 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 ShellKillResult +public sealed class RemoteNotifySteerableChangedResult { - /// Whether the signal was sent successfully. - [JsonPropertyName("killed")] - public bool Killed { get; set; } } -/// Identifier of a process previously returned by "shell.exec" and the signal to send. +/// New remote-steerability state to persist as a `session.remote_steerable_changed` event. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ShellKillRequest +internal sealed class RemoteNotifySteerableChangedRequest { - /// Process identifier returned by shell.exec. - [JsonPropertyName("processId")] - public string ProcessId { get; set; } = string.Empty; + /// 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; - - /// Signal to send (default: SIGTERM). - [JsonPropertyName("signal")] - public ShellKillSignal? Signal { get; set; } } -/// Result of a user-requested shell command. +/// Current sharing status and shareable GitHub URL for a session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UserRequestedShellCommandResult +public sealed class VisibilityGetResult { - /// 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; + /// 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 the command completed successfully. - [JsonPropertyName("success")] - public bool Success { 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; } - /// Tool call id emitted for the shell execution. - [JsonPropertyName("toolCallId")] - public string ToolCallId { get; set; } = string.Empty; + /// 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; } } -/// User-requested shell command and cancellation handle. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ShellExecuteUserRequestedRequest +internal sealed class SessionVisibilityGetRequest { - /// 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; } -/// Cancellation result for a user-requested shell command. +/// Effective sharing status and shareable GitHub URL after updating session visibility. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CancelUserRequestedShellCommandResult +public sealed class VisibilitySetResult { - /// Whether an in-flight execution was found and signalled to cancel. - [JsonPropertyName("cancelled")] - public bool Cancelled { get; 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; } + + /// 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; } } -/// User-requested shell execution cancellation handle. +/// Desired sharing status for the session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ShellCancelUserRequestedRequest +internal sealed class VisibilitySetRequest { - /// 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; + + /// 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; } } -/// Post-compaction context window usage breakdown. +/// Scheduled prompt entry with ID, timing (`intervalMs`, `cron`, or `at`), prompt text, recurrence, and next run time. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryCompactContextWindow +public sealed class ScheduleEntry { - /// 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; } + /// Absolute fire time (epoch milliseconds) for a one-shot calendar schedule. + [JsonPropertyName("at")] + public long? At { get; set; } - /// Current number of messages in the conversation. - [JsonPropertyName("messagesLength")] - public long MessagesLength { get; set; } + /// 5-field cron expression for a recurring calendar schedule, evaluated in `tz`. + [JsonPropertyName("cron")] + public string? Cron { get; set; } - /// Token count from system message(s). - [JsonPropertyName("systemTokens")] - public long? SystemTokens { 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; } - /// Maximum token count for the model's context window. - [JsonPropertyName("tokenLimit")] - public long TokenLimit { 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; } - /// Token count from tool definitions. - [JsonPropertyName("toolDefinitionsTokens")] - public long? ToolDefinitionsTokens { get; set; } -} + /// Interval between scheduled ticks, in milliseconds (relative-interval schedules). + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonPropertyName("intervalMs")] + public TimeSpan? Interval { get; set; } -/// 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 HistoryCompactResult -{ - /// Post-compaction context window usage breakdown. - [JsonPropertyName("contextWindow")] - public HistoryCompactContextWindow? ContextWindow { get; set; } + /// ISO 8601 timestamp when the next tick is scheduled to fire. + [JsonPropertyName("nextRunAt")] + public DateTimeOffset NextRunAt { get; set; } - /// Number of messages removed during compaction. - [JsonPropertyName("messagesRemoved")] - public long MessagesRemoved { get; set; } + /// Prompt text that gets enqueued on every tick. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; - /// Whether compaction completed successfully. - [JsonPropertyName("success")] - public bool Success { get; set; } + /// Whether the schedule re-arms after each tick (`/every`) or fires once (`/after`). + [JsonPropertyName("recurring")] + public bool Recurring { 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; } + /// 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; } - /// Number of tokens freed by compaction. - [JsonPropertyName("tokensRemoved")] - public long TokensRemoved { get; set; } + /// IANA timezone the `cron` expression is evaluated in. + [JsonPropertyName("tz")] + public string? Tz { get; set; } } -/// RPC data type for SessionHistoryCompact operations. +/// Snapshot of the currently active recurring prompts for this session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionHistoryCompactRequest +public sealed class ScheduleList { - /// 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; } + /// Active scheduled prompts, ordered by id. + [JsonPropertyName("entries")] + public IList Entries { get => field ??= []; set; } } -/// RPC data type for SessionHistoryCompactRequestWithSession operations. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionHistoryCompactRequestWithSession +internal sealed class SessionScheduleListRequest { - /// 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; } - /// 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. +/// Identifies the target session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryTruncateResult +internal sealed class SessionScheduleHydrateRequest { - /// 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Identifier of the event to truncate to; this event and all later events are removed. +/// Whether the session currently has an active self-paced schedule. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class HistoryTruncateRequest +internal sealed class ScheduleHasSelfPacedResult { - /// 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; + /// True when at least one active schedule is self-paced. + [JsonPropertyName("hasSelfPaced")] + public bool HasSelfPaced { get; set; } +} +/// Identifies the target session. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +internal sealed class SessionScheduleHasSelfPacedRequest +{ /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// A root user turn that the session can rewind to. +/// Result of registering or re-arming a scheduled prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryRewindPoint +internal sealed class ScheduleAddResult { - /// 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; } - - /// Whether this turn was an automatically injected autopilot continuation. - [JsonPropertyName("isAutopilotContinuation")] - public bool IsAutopilotContinuation { get; set; } - - /// Lines added by this turn's captured file changes. - [JsonPropertyName("linesAdded")] - public long LinesAdded { get; set; } - - /// Lines removed by this turn's captured file changes. - [JsonPropertyName("linesRemoved")] - public long LinesRemoved { get; set; } - - /// ISO timestamp of the user turn. - [JsonPropertyName("timestamp")] - public string Timestamp { get; set; } = string.Empty; - - /// Whether this turn itself captured any file changes. - [JsonPropertyName("turnChangedFiles")] - public bool TurnChangedFiles { get; set; } + /// The registered or updated schedule entry. + [JsonPropertyName("entry")] + public ScheduleEntry? Entry { get; set; } - /// User-visible message text for the turn. - [JsonPropertyName("userMessage")] - public string UserMessage { get; set; } = string.Empty; + /// User-facing validation error, when registration failed. + [JsonPropertyName("error")] + public string? Error { get; set; } } -/// Rewind points and file-change-tracking availability for the session. +/// Register a relative-interval scheduled prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryListRewindPointsResult +internal sealed class ScheduleAddRequest { - /// Whether this session captured file changes from its first turn. - [JsonPropertyName("fileChangeTrackingEnabled")] - public bool FileChangeTrackingEnabled { get; set; } + /// Optional display-only prompt label. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { get; set; } - /// Root user turns in chronological order. Empty when `unavailableReason` is set. - [JsonPropertyName("points")] - public IList Points { get => field ??= []; set; } + /// Human-readable interval such as `30s`, `5m`, or `2h`. + [JsonPropertyName("interval")] + public string Interval { get; set; } = string.Empty; - /// 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; } -} + /// 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; } -/// 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; } -/// A file that a conversation-and-files rewind would restore. +/// Register a cron scheduled prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryRewindFilePreview +internal sealed class ScheduleAddCronRequest { - /// Aggregate change made across the discarded turns. - [JsonPropertyName("changeType")] - public HistoryRewindChangeType ChangeType { get; set; } + /// 5-field cron expression. + [JsonPropertyName("cron")] + public string Cron { get; set; } = string.Empty; - /// Lines added across the discarded turns. - [JsonPropertyName("linesAdded")] - public long LinesAdded { get; set; } + /// Optional display-only prompt label. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { get; set; } - /// Lines removed across the discarded turns. - [JsonPropertyName("linesRemoved")] - public long LinesRemoved { get; set; } + /// Prompt text to enqueue when the schedule fires. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; - /// Absolute path of the captured file. - [JsonPropertyName("path")] - public string Path { 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; } } -/// Files and aggregate changes for a prospective rewind. +/// Register an absolute-time scheduled prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryPreviewRewindResult +internal sealed class ScheduleAddAtRequest { - /// 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; } - - /// Number of unique files in the preview. - [JsonPropertyName("fileCount")] - public long FileCount { get; set; } + /// Epoch milliseconds when the prompt should fire. + [JsonPropertyName("at")] + public long At { get; set; } - /// Files ordered by path. - [JsonPropertyName("files")] - public IList Files { get => field ??= []; set; } + /// Optional display-only prompt label. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { 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; } -} + /// Prompt text to enqueue when the schedule fires. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; -/// 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; + /// 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; } -/// 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; } -} - -/// Structured outcome of a rewind request. +/// Register a self-paced scheduled prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryRewindResult +internal sealed class ScheduleAddSelfPacedRequest { - /// 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; } - - /// 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; } - - /// 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 display-only prompt label. + [JsonPropertyName("displayPrompt")] + public string? DisplayPrompt { 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; } + /// Prompt text to enqueue when the schedule fires. + [JsonPropertyName("prompt")] + public string Prompt { 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; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Boundary and mode for rewinding session history. +/// Re-arm a self-paced scheduled prompt. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class HistoryRewindRequest +internal sealed class ScheduleRearmSelfPacedRequest { - /// ID of the user.message event that begins the discarded suffix. - [JsonPropertyName("eventId")] - public string EventId { get; set; } = string.Empty; + /// Epoch milliseconds when the prompt should next fire. + [JsonPropertyName("at")] + public long At { get; set; } - /// Whether to rewind only conversation history or also restore captured files. - [JsonPropertyName("mode")] - public HistoryRewindMode Mode { 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; } -/// Indicates whether an in-progress background compaction was cancelled. +/// 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 HistoryCancelBackgroundCompactionResult +public sealed class ScheduleStopResult { - /// 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; } + /// The removed entry, or omitted if no entry matched. + [JsonPropertyName("entry")] + public ScheduleEntry? Entry { get; set; } } -/// Identifies the target session. +/// Identifier of the scheduled prompt to remove. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionHistoryCancelBackgroundCompactionRequest +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; } -/// Indicates whether an in-progress manual compaction was aborted. +/// 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 HistoryAbortManualCompactionResult +public sealed class ProviderTokenAcquireResult { - /// 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; } + /// The bearer token value (without the `Bearer ` prefix). + [JsonPropertyName("token")] + public string Token { get; set; } = string.Empty; } -/// Identifies the target session. +/// 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)] -internal sealed class SessionHistoryAbortManualCompactionRequest +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; } -/// Markdown summary of the conversation context (empty when not available). +/// Result returned by an extension workflow closure. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistorySummarizeForHandoffResult +public sealed class WorkflowExecuteResult { - /// 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; + /// Workflow result value. + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } } -/// Identifies the target session. +/// Parameters sent to the owning extension to execute a workflow closure. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionHistorySummarizeForHandoffRequest +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; } -/// What a successful clear removed. A clear that could not be applied rejects instead of reporting a count. +/// Parameters for cooperatively aborting a workflow body. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class HistoryClearContextResult +public sealed class WorkflowAbortRequest { - /// 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; } -} + /// Opaque token identifying the execution attempt to abort. + [JsonPropertyName("executionToken")] + public string ExecutionToken { get; set; } = string.Empty; -/// 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; + /// Workflow run identifier. + [JsonPropertyName("runId")] + public string RunId { get; set; } = string.Empty; /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// User-facing pending queue entry, with kind and display text for a queued message, slash command, or model change. +/// Whether the client authoritatively confirmed its external work stopped. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueuePendingItems +public sealed class ClientTaskCancelResult { - /// 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; } - - /// Human-readable text to display for this queue entry in the UI. - [JsonPropertyName("displayText")] - public string DisplayText { get; set; } = string.Empty; - - /// Stable opaque id for the canonical queued item. Batch rows share one id. - [JsonPropertyName("id")] - public string Id { 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; } - - /// 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; } - - /// 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; } + /// True only when the owner confirms that external work stopped before responding. + [JsonPropertyName("cancelled")] + public bool Cancelled { get; set; } } -/// Snapshot of the session's pending queued items and immediate-steering messages. +/// Runtime-to-owner cancellation request for a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueuePendingItemsResult +public sealed class ClientTaskCancelRequest { - /// 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; } + /// Opaque identifier shared by coalesced cancellation callers. + [JsonPropertyName("cancellationId")] + public string CancellationId { 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; } + /// Owner-scoped task key included for correlation. + [JsonPropertyName("clientTaskId")] + public string ClientTaskId { get; set; } = string.Empty; - /// Display text for messages currently in the immediate steering queue (interjections sent during a running turn). - [JsonPropertyName("steeringMessages")] - public IList SteeringMessages { get => field ??= []; set; } + /// Canonical runtime-generated task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; - /// 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; } -} + /// Reason the runtime requests cancellation. + [JsonPropertyName("reason")] + public ClientTaskCancelReason Reason { get; set; } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueuePendingItemsRequest -{ - /// Target session identifier. + /// Session that owns the client task. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Internal snapshot of native queue state for local session orchestration. +/// Describes a filesystem error. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueSnapshotResult +public sealed class SessionFsError { - /// Queue item identifier of a model switch that has been dequeued but not yet applied. - [JsonPropertyName("inFlightModelChangeId")] - public string? InFlightModelChangeId { get; set; } + /// Error classification. + [JsonPropertyName("code")] + public SessionFsErrorCode Code { get; set; } - /// Insertion orders for queued items, aligned with `items`. - [JsonPropertyName("itemOrders")] - public IList? ItemOrders { get; set; } + /// Free-form detail about the error, for logging/diagnostics. + [JsonPropertyName("message")] + public string? Message { get; set; } - /// User-facing pending items in FIFO order. - [JsonPropertyName("items")] - public IList Items { get => field ??= []; 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; } +} - /// Insertion orders for immediate steering messages, aligned with `steeringMessages`. - [JsonPropertyName("steeringMessageOrders")] - public IList? SteeringMessageOrders { 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; - /// Immediate steering messages waiting for an active turn. - [JsonPropertyName("steeringMessages")] - public IList SteeringMessages { get => field ??= []; set; } + /// Describes a filesystem error. + [JsonPropertyName("error")] + public SessionFsError? Error { get; set; } } -/// Identifies the target session. +/// Path of the file to read from the client-provided session filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueueSnapshotRequest +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; } -/// Result of moving a queued item. +/// File bytes as standard base64, or a filesystem error if the read failed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueMoveItemResult +public sealed class SessionFsReadFileBytesResult { - /// True when the item changed position; false when it was already at the requested position. - [JsonPropertyName("changed")] - public bool Changed { get; set; } + /// 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; } } -/// Parameters for moving a queued item by stable id. +/// Path of the binary file to read from the client-provided session filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueMoveItemRequest +public sealed class SessionFsReadFileBytesRequest { - /// Stable opaque queued-item id. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// 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; - - /// Zero-based target position in the public visible queue. Values outside the queue clamp to an end. - [JsonPropertyName("toPosition")] - public long ToPosition { get; set; } -} - -/// Result of inserting a queued message. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueInsertAtResult -{ - /// Fresh stable opaque id assigned to the inserted item. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; } -/// Serializable message fields accepted by queue.insertAt. +/// File path, content to write, and optional mode for the client-provided session filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueInsertMessage +public sealed class SessionFsWriteFileRequest { - /// 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; } - - /// Whether the message is billable. - [JsonPropertyName("billable")] - public bool? Billable { get; set; } - - /// Accepted for internal SendOptions compatibility but ignored; delivery is derived from current session activity. - [JsonPropertyName("delivery")] - public string? Delivery { get; set; } - - /// Optional user-facing display text. - [JsonPropertyName("displayPrompt")] - public string? DisplayPrompt { get; set; } + /// Content to write. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; - /// Accepted for SendOptions compatibility but ignored; inserted items always use queued delivery semantics. + /// Optional POSIX-style mode for newly created files. [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; - - /// 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; } + public long? Mode { 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; } + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Accepted for SendOptions compatibility but ignored; insertion scheduling is controlled by the queue drain state. - [JsonPropertyName("wait")] - public bool? Wait { get; set; } + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; } -/// Parameters for inserting a queued message at a public visible position. +/// File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueInsertAtRequest +public sealed class SessionFsWriteFileBytesRequest { - /// Queued message contents and delivery metadata. - [JsonPropertyName("message")] - public QueueInsertMessage Message { get => field ??= new(); set; } + /// Exact file bytes encoded as standard base64. + [JsonPropertyName("content")] + public string Content { 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; } + /// 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; } -/// Result of removing a queued item. +/// 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 QueueRemoveAtResult +public sealed class SessionFsAppendFileRequest { - /// True when the addressed item was removed. - [JsonPropertyName("removed")] - public bool Removed { get; set; } -} + /// Content to append. + [JsonPropertyName("content")] + public string Content { get; set; } = string.Empty; -/// Parameters for removing a queued item by stable id. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueRemoveAtRequest -{ - /// Stable opaque ID of the queued item to remove. - [JsonPropertyName("id")] - public string Id { 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; } -/// Result of editing a queued message. +/// Indicates whether the requested path exists in the client-provided session filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueUpdateTextResult +public sealed class SessionFsExistsResult { - /// True when the stored text changed. - [JsonPropertyName("updated")] - public bool Updated { get; set; } + /// Whether the path exists. + [JsonPropertyName("exists")] + public bool Exists { get; set; } } -/// Parameters for editing a single queued message. +/// Path to test for existence in the client-provided session filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueUpdateTextRequest +public sealed class SessionFsExistsRequest { - /// 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; + /// 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; } -/// Result of withdrawing a user message. +/// Filesystem metadata for the requested path, or a filesystem error if the stat failed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueWithdrawMessageResult +public sealed class SessionFsStatResult { - /// 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; } + /// ISO 8601 timestamp of creation. + [JsonPropertyName("birthtime")] + public DateTimeOffset Birthtime { get; set; } - /// True when the message left the queue or, for a running turn, history. - [JsonPropertyName("removed")] - public bool Removed { 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; } } -/// Conditional withdrawal of a single user message, from its queue or from the running turn it started. +/// Path whose metadata should be returned from the client-provided session filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueWithdrawMessageRequest +public sealed class SessionFsStatRequest { - /// 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; + /// 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; } -/// Append to one pending steering message without changing its identity or delivery position. +/// Directory path to create in the client-provided session filesystem, with options for recursive creation and POSIX mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueAppendSteeringRequest +public sealed class SessionFsMkdirRequest { - /// 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; + /// Optional POSIX-style mode for newly created directories. + [JsonPropertyName("mode")] + public long? Mode { get; set; } - /// 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; + /// Path using SessionFs conventions. + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; - /// Text to append after a blank line. - [JsonPropertyName("prompt")] - public string Prompt { 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; } -/// Result of duplicating a queued item. +/// Names of entries in the requested directory, or a filesystem error if the read failed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueDuplicateAtResult +public sealed class SessionFsReaddirResult { - /// Fresh stable opaque id assigned to the duplicate. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// Entry names in the directory. + [JsonPropertyName("entries")] + public IList Entries { get => field ??= []; set; } + + /// Describes a filesystem error. + [JsonPropertyName("error")] + public SessionFsError? Error { get; set; } } -/// Parameters for duplicating a queued item. +/// Directory path whose entries should be listed from the client-provided session filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueDuplicateAtRequest +public sealed class SessionFsReaddirRequest { - /// Stable opaque ID of the queued item to duplicate. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// 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; } -/// 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. +/// Directory entry returned by session filesystem `readdirWithTypes`, with name and entry type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueSetDrainPausedRequest +public sealed class SessionFsReaddirWithTypesEntry { - /// Whether queued-lane draining should be paused. - [JsonPropertyName("paused")] - public bool Paused { get; set; } + /// Entry name. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Entry type. + [JsonPropertyName("type")] + public SessionFsReaddirWithTypesEntryType Type { get; set; } } -/// Result of trying to steer a queued message into a live turn. +/// 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 QueueSendNowResult +public sealed class SessionFsReaddirWithTypesResult { - /// True when the item was accepted into the steering lane; false when no main turn was live. - [JsonPropertyName("steered")] - public bool Steered { get; set; } + /// Directory entries with type information. + [JsonPropertyName("entries")] + public IList Entries { get => field ??= []; set; } + + /// Describes a filesystem error. + [JsonPropertyName("error")] + public SessionFsError? Error { get; set; } } -/// Parameters for steering a queued message into a live turn. +/// Directory path whose entries (with type information) should be listed from the client-provided session filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueSendNowRequest +public sealed class SessionFsReaddirWithTypesRequest { - /// Stable opaque ID of the queued item to steer into the live turn. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// 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; } -/// Whether the native queue has pending work. +/// Path to remove from the client-provided session filesystem, with options for recursive removal and force. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueHasPendingResult +public sealed class SessionFsRmRequest { - /// True when queued or immediate native work is pending. - [JsonPropertyName("hasPending")] - public bool HasPending { get; set; } + /// 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; } -/// Identifies the target session. +/// Source and destination paths for renaming or moving an entry in the client-provided session filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueueHasPendingRequest +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; } -/// Whether a deferred-idle drain should run. +/// Query results including rows, columns, and rows affected, or a filesystem error if execution failed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueBeginDeferredIdleDrainResult +public sealed class SessionFsSqliteQueryResult { - /// True when the host should run finishDeferredIdleDrain asynchronously. - [JsonPropertyName("shouldDrain")] - public bool ShouldDrain { get; set; } + /// 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; } } -/// Inputs for starting a deferred-idle drain. +/// 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)] -internal sealed class QueueBeginDeferredIdleDrainRequest +public sealed class SessionFsSqliteQueryRequest { - /// Whether the host still has active background work. - [JsonPropertyName("activeBackgroundWork")] - public bool ActiveBackgroundWork { get; set; } + /// 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; } -/// Action selected by the native deferred-idle drain. +/// Classified SQLite transaction failure. busyOrLocked guarantees rollback; postCommitAmbiguous must never be retried. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueFinishDeferredIdleDrainResult +public sealed class SessionFsSqliteTransactionError { - /// Whether the deferred idle was caused by an aborted foreground turn. - [JsonPropertyName("aborted")] - public bool Aborted { get; set; } + /// Machine-readable classification of the transaction failure. + [JsonPropertyName("errorClass")] + public SessionFsSqliteTransactionErrorClass ErrorClass { get; set; } - /// One of none, processQueue, or emitSessionIdle. - [JsonPropertyName("action")] - public string Action { get; set; } = string.Empty; + /// Human-readable transaction failure message. + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; } -/// Inputs for completing a deferred-idle drain. +/// Per-statement results, or a classified transaction error. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueFinishDeferredIdleDrainRequest +public sealed class SessionFsSqliteTransactionResult { - /// 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; } + /// Classified transaction failure, when execution did not succeed. + [JsonPropertyName("error")] + public SessionFsSqliteTransactionError? Error { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Per-statement query results in input order. + [JsonPropertyName("results")] + public IList Results { get => field ??= []; set; } } -/// Inputs for marking session.idle deferred in native state. +/// One statement in an atomic SQLite transaction. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueDeferSessionIdleRequest +public sealed class SessionFsSqliteTransactionStatement { - /// Whether the deferred idle was caused by an aborted foreground turn. - [JsonPropertyName("aborted")] - public bool Aborted { get; set; } + /// 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 a user-facing pending item was removed. +/// Indicates whether the per-session SQLite database already exists. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class QueueRemoveMostRecentResult +public sealed class SessionFsSqliteExistsResult { - /// 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; } + /// Whether the session database already exists. + [JsonPropertyName("exists")] + public bool Exists { get; set; } } /// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueueRemoveMostRecentRequest +public sealed class SessionFsSqliteExistsRequest { /// Target session identifier. [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; } -/// Identifies the target session. +/// Canvas open result returned by the provider. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueueClearRequest +public sealed class CanvasProviderOpenResult { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } } -/// Internal filter for consuming queued system notifications. +/// Host capabilities. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueConsumeSystemNotificationsRequest +public sealed class CanvasHostContextCapabilities { - /// Opaque runtime-owned filter object. - [JsonPropertyName("filter")] - public JsonElement Filter { get; set; } + /// Whether canvas rendering is supported. + [JsonPropertyName("canvases")] + public bool? Canvases { get; set; } +} - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; +/// 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; } } -/// Result of enqueueing the resume-pending wake item. +/// Session context supplied by the runtime. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class QueueEnqueueResumePendingResult +public sealed class CanvasSessionContext { - /// True when a wake item was newly queued. - [JsonPropertyName("queued")] - public bool Queued { get; set; } + /// Active session working directory, when known. + [JsonPropertyName("workingDirectory")] + public string? WorkingDirectory { get; set; } } -/// Identifies the target session. +/// Canvas open parameters sent to the provider. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueueEnqueueResumePendingRequest +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; } -/// Identifies the target session. +/// Canvas close parameters sent to the provider. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionQueueProcessRequest +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; } -/// Cursor, batch size, and optional long-poll/filter parameters for reading session events. +/// Canvas action invocation parameters sent to the provider. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class EventLogReadRequest +public sealed class CanvasProviderInvokeActionRequest { - /// 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; } + /// Action name to invoke. + [JsonPropertyName("actionName")] + public string ActionName { get; set; } = string.Empty; - /// 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; } + /// Provider-local canvas identifier. + [JsonPropertyName("canvasId")] + public string CanvasId { get; set; } = string.Empty; - /// 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; } + /// Owning provider identifier. + [JsonPropertyName("extensionId")] + public string ExtensionId { get; set; } = string.Empty; - /// 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; } + /// Host context supplied by the runtime. + [JsonPropertyName("host")] + public CanvasHostContext? Host { get; set; } - /// 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; } + /// Action input. + [JsonPropertyName("input")] + public JsonElement? Input { get; set; } - /// Maximum number of events to return in this batch (1–1000, default 200). - [JsonPropertyName("max")] - public long? Max { 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; +} - /// Either '*' to receive all event types, or a non-empty list of event types to receive. - [JsonPropertyName("types")] - public JsonElement? Types { 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; } - /// 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; } + /// 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; } } -/// 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). +/// Opaque integrator-owned process launch profile for one extension entrypoint. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class EventLogTailResult +public sealed class ExtensionLaunchProfile { - /// 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; + /// 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; } -/// Identifies the target session. +/// The launch profile for a supported entrypoint. Omit launch when the provider does not support the entrypoint. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionEventLogTailRequest +public sealed class ExtensionLaunchProviderResolveResult { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Opaque launch profile, omitted when this provider does not support the entrypoint. + [JsonPropertyName("launch")] + public ExtensionLaunchProfile? Launch { get; set; } } -/// Opaque handle representing an event-type interest registration. +/// 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 RegisterEventInterestResult +public sealed class ExtensionLaunchProviderResolveRequest { - /// 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; + /// 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; } } -/// Event type to register consumer interest for, used by runtime gating logic. +/// 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)] -internal sealed class RegisterEventInterestParams +public sealed class LlmInferenceHttpRequestStartResult { - /// 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. +/// 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; } = string.Empty; + 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; } -/// Indicates whether the operation succeeded. +/// 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 EventLogReleaseInterestResult +public sealed class LlmInferenceHttpRequestChunkResult { - /// Whether the operation succeeded. - [JsonPropertyName("success")] - public bool Success { get; set; } } -/// Opaque handle previously returned by `registerInterest` to release. +/// A request body chunk or cancellation signal. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ReleaseEventInterestParams +public sealed class LlmInferenceHttpRequestChunkRequest { - /// 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; + /// 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; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Matches the requestId from the originating httpRequestStart frame. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; } -/// Request count and cost metrics for this model. +/// Client environment metadata describing the process that produced a telemetry event. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsModelMetricRequests +public sealed class GitHubTelemetryClientInfo { - /// User-initiated premium request cost (with multiplier applied). - [JsonPropertyName("cost")] - public double Cost { get; set; } + /// Copilot CLI version string. + [JsonPropertyName("cli_version")] + public string CliVersion { get; set; } = string.Empty; - /// Number of API requests made with this model. - [JsonPropertyName("count")] - public long Count { get; set; } -} + /// Name of the client application. + [JsonPropertyName("client_name")] + public string? ClientName { 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; } -} + /// Type of client. + [JsonPropertyName("client_type")] + public string? ClientType { get; set; } -/// Token usage metrics for this model. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsModelMetricUsage -{ - /// Total tokens read from prompt cache. - [JsonPropertyName("cacheReadTokens")] - public long CacheReadTokens { get; set; } + /// Copilot subscription plan, when known. + [JsonPropertyName("copilot_plan")] + public string? CopilotPlan { get; set; } - /// Total tokens written to prompt cache. - [JsonPropertyName("cacheWriteTokens")] - public long CacheWriteTokens { get; set; } + /// Number of logical CPU cores on the host. + [JsonPropertyName("cpu_count")] + public long? CpuCount { get; set; } - /// Total input tokens consumed. - [JsonPropertyName("inputTokens")] - public long InputTokens { get; set; } + /// Distinct CPU model names for the host, comma-separated. + [JsonPropertyName("cpu_model")] + public string? CpuModel { get; set; } - /// Total output tokens produced. - [JsonPropertyName("outputTokens")] - public long OutputTokens { get; set; } + /// Stable machine identifier for the device. + [JsonPropertyName("dev_device_id")] + public string? DevDeviceId { get; set; } - /// Total output tokens used for reasoning. - [JsonPropertyName("reasoningTokens")] - public long? ReasoningTokens { 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; } -/// Per-model usage metrics, including request counts/costs, token usage, nano-AI units, and per-token-type details. +/// 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 UsageMetricsModelMetric +public sealed class GitHubTelemetryEvent { - /// 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; } + /// Client environment metadata. + [JsonPropertyName("client")] + public GitHubTelemetryClientInfo? Client { get; set; } - /// Request count and cost metrics for this model. - [JsonPropertyName("requests")] - public UsageMetricsModelMetricRequests Requests { get => field ??= new(); set; } + /// Copilot tracking ID for user-level attribution. + [JsonPropertyName("copilot_tracking_id")] + public string? CopilotTrackingId { get; set; } - /// Token count details per type. - [JsonPropertyName("tokenDetails")] - public IDictionary? TokenDetails { get; set; } + /// Timestamp when the event was created (ISO 8601 format). + [JsonPropertyName("created_at")] + public string? CreatedAt { get; set; } - /// Accumulated nano-AI units cost for this model. - [JsonPropertyName("totalNanoAiu")] - public double? TotalNanoAiu { get; set; } + /// Experiment assignment context. + [JsonPropertyName("exp_assignment_context")] + public string? ExpAssignmentContext { get; set; } - /// Token usage metrics for this model. - [JsonPropertyName("usage")] - public UsageMetricsModelMetricUsage Usage { get => field ??= new(); set; } -} + /// Feature flags enabled for this session, as a map from flag to value. + [JsonPropertyName("features")] + public IDictionary? Features { get; set; } -/// 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 UsageMetricsAgentMetric -{ - /// 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; } + /// Event type/kind (e.g. get_completion_with_tools_turn, tool_call_executed). + [JsonPropertyName("kind")] + public string Kind { get; set; } = string.Empty; - /// Configured agent name, when this is a subagent. - [JsonPropertyName("agentName")] - public string? AgentName { get; set; } + /// Numeric metrics as a map from key to value. + [JsonPropertyName("metrics")] + public IDictionary Metrics { get => field ??= new Dictionary(); set; } - /// Per-model usage for this agent, keyed by model identifier. - [JsonPropertyName("modelMetrics")] - public IDictionary ModelMetrics { get => field ??= new Dictionary(); set; } + /// Reference to the model call that produced this event. + [JsonPropertyName("model_call_id")] + public string? ModelCallId { get; set; } - /// Time spent in model API calls by this agent, in milliseconds. - [JsonConverter(typeof(MillisecondsTimeSpanConverter))] - [JsonPropertyName("totalApiDurationMs")] - public TimeSpan TotalApiDuration { get; set; } + /// String-valued properties as a map from key to value. + [JsonPropertyName("properties")] + public IDictionary Properties { get => field ??= new Dictionary(); set; } - /// Accumulated nano-AI units cost for this agent. - [JsonPropertyName("totalNanoAiu")] - public double TotalNanoAiu { get; set; } + /// Session identifier the event belongs to. + [JsonPropertyName("session_id")] + public string? SessionId { get; set; } } -/// Aggregated code change metrics. +/// 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 UsageMetricsCodeChanges +public sealed class GitHubTelemetryNotification { - /// 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; } + /// The telemetry event, in the runtime's native GitHub-shaped telemetry format. + [JsonPropertyName("event")] + public GitHubTelemetryEvent Event { get => field ??= new(); set; } - /// Total lines of code added. - [JsonPropertyName("linesAdded")] - public long LinesAdded { 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; } - /// Total lines of code removed. - [JsonPropertyName("linesRemoved")] - public long LinesRemoved { 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; } } -/// Session-wide token-detail entry containing the accumulated token count for one token type. +/// SDK host response to a GitHub credential request. +/// Polymorphic base type discriminated by kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageMetricsTokenDetail +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(GitHubTokenAcquireResultToken), "token")] +[JsonDerivedType(typeof(GitHubTokenAcquireResultCancelled), "cancelled")] +public partial class GitHubTokenAcquireResult { - /// Accumulated token count for this token type. - [JsonPropertyName("tokenCount")] - public long TokenCount { get; set; } + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; } -/// Accumulated session usage metrics, including premium request cost, token counts, model breakdown, and code-change totals. + +/// The token variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class UsageGetMetricsResult +public partial class GitHubTokenAcquireResultToken : GitHubTokenAcquireResult { - /// Per-agent usage metrics, keyed by agent instance identifier. The main conversation uses the stable key `main`. - [JsonPropertyName("agentMetrics")] - public IDictionary? AgentMetrics { get; set; } + /// + [JsonIgnore] + public override string Kind => "token"; - /// Aggregated code change metrics. - [JsonPropertyName("codeChanges")] - public UsageMetricsCodeChanges CodeChanges { get => field ??= new(); set; } + /// GitHub access token acquired by the SDK host. + [JsonPropertyName("accessToken")] + public required string AccessToken { get; set; } - /// Currently active model identifier. - [JsonPropertyName("currentModel")] - public string? CurrentModel { 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; } - /// Input tokens from the most recent main-agent API call. - [JsonPropertyName("lastCallInputTokens")] - public long LastCallInputTokens { get; set; } + /// OAuth token type. Defaults to bearer when omitted. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("tokenType")] + public string? TokenType { get; set; } +} - /// Output tokens from the most recent main-agent API call. - [JsonPropertyName("lastCallOutputTokens")] - public long LastCallOutputTokens { get; set; } +/// The cancelled variant of . +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public partial class GitHubTokenAcquireResultCancelled : GitHubTokenAcquireResult +{ + /// + [JsonIgnore] + public override string Kind => "cancelled"; +} - /// Per-model token and request metrics, keyed by model identifier. - [JsonPropertyName("modelMetrics")] - public IDictionary ModelMetrics { get => field ??= new Dictionary(); set; } +/// 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; - /// ISO 8601 timestamp when the session started. - [JsonPropertyName("sessionStartTime")] - public DateTimeOffset SessionStartTime { get; set; } + /// Why the runtime is requesting a GitHub credential. + [JsonPropertyName("reason")] + public GitHubTokenAcquireReason Reason { get; set; } - /// Session-wide per-token-type accumulated token counts. - [JsonPropertyName("tokenDetails")] - public IDictionary? TokenDetails { get; set; } + /// Opaque identifier generated by the SDK for this callback registration. + [JsonPropertyName("registrationId")] + public string RegistrationId { get; set; } = string.Empty; - /// Total time spent in model API calls (milliseconds). - [JsonConverter(typeof(MillisecondsTimeSpanConverter))] - [JsonPropertyName("totalApiDurationMs")] - public TimeSpan TotalApiDuration { get; set; } + /// Session receiving the token. Absent only before a cloud session has been assigned its id. + [JsonPropertyName("sessionId")] + public string? SessionId { get; set; } +} - /// Session-wide accumulated nano-AI units cost. - [JsonPropertyName("totalNanoAiu")] - public double? TotalNanoAiu { 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; - /// Total user-initiated premium request cost across all models (may be fractional due to multipliers). - [JsonPropertyName("totalPremiumRequestCost")] - public double TotalPremiumRequestCost { get; set; } + /// Fresh explicit user decision. There is no default. + [JsonPropertyName("decision")] + public InstallationDecision Decision { get; set; } - /// Raw count of user-initiated API requests. - [JsonPropertyName("totalUserRequests")] - public long TotalUserRequests { get; set; } + /// Exact review commitment from the request. + [JsonPropertyName("reviewFingerprint")] + public string ReviewFingerprint { get; set; } = string.Empty; } -/// Identifies the target session. +/// Only resource kinds with an implemented installation engine have a review variant. +/// Polymorphic base type discriminated by resource. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionUsageGetMetricsRequest +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "resource", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(InstallationReviewMcp), "mcp")] +[JsonDerivedType(typeof(InstallationReviewSkill), "skill")] +public partial class InstallationReview { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// The type discriminator. + [JsonPropertyName("resource")] + public virtual string Resource { get; set; } = string.Empty; } -/// Prediction result. Available results include prediction details; unavailable results include an explicit reason. -/// Polymorphic base type discriminated by kind. + +/// 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 = "kind", + TypeDiscriminatorPropertyName = "action", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(SessionLimitPredictionResultAvailable), "available")] -[JsonDerivedType(typeof(SessionLimitPredictionResultUnavailable), "unavailable")] -public partial class SessionLimitPredictionResult +[JsonDerivedType(typeof(McpInstallationReviewInstall), "install")] +[JsonDerivedType(typeof(McpInstallationReviewUninstall), "uninstall")] +public partial class McpInstallationReview { /// The type discriminator. - [JsonPropertyName("kind")] - public virtual string Kind { get; set; } = string.Empty; + [JsonPropertyName("action")] + public virtual string Action { get; set; } = string.Empty; } -/// Baseline data provenance for a prediction. +/// +/// 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 SessionLimitPredictionBaselineData +public sealed class McpInstallationRemoteConfiguration { - /// End of the baseline data slice. - [JsonPropertyName("windowEnd")] - public string WindowEnd { get; set; } = string.Empty; + /// + /// 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; } - /// Start of the baseline data slice. - [JsonPropertyName("windowStart")] - public string WindowStart { get; set; } = string.Empty; -} + /// Configured tool selection, not permission to invoke those tools. + [JsonPropertyName("tools")] + public IList Tools { get => field ??= []; 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; } + /// Transport in the effective persisted remote configuration. + [JsonPropertyName("transport")] + public McpPlanRemoteTransport Transport { get; set; } - /// Semantic usage tier. - [JsonPropertyName("tier")] - public SessionLimitPredictionTier Tier { get; set; } + /// Exact resolved endpoint, without templates or secret placeholders. + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; } -/// Explainable AI-credit session-limit prediction. +/// The install variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionLimitPredictionDetails +public partial class McpInstallationReviewInstall : McpInstallationReview { - /// Baseline data provenance. - [JsonPropertyName("baselineData")] - public SessionLimitPredictionBaselineData BaselineData { get => field ??= new(); set; } + /// + [JsonIgnore] + public override string Action => "install"; - /// Client population used for the prediction. - [JsonPropertyName("clientType")] - public SessionLimitPredictionClientType ClientType { get; set; } + /// Catalogue identity retained from the bound candidate when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("catalogue")] + public InstallationCatalogueIdentity? Catalogue { get; set; } - /// Resolved model family when known. - [JsonPropertyName("family")] - public string? Family { get; set; } + /// Original catalogue trust metadata, not a verification claim. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("catalogueTrust")] + public CatalogTrustSnapshot? CatalogueTrust { get; set; } - /// Model identifier used for lookup. - [JsonPropertyName("modelId")] - public string ModelId { get; set; } = string.Empty; + /// The configuration change for the selected alternative only. + [JsonPropertyName("configurationChange")] + public required McpPlanConfigurationChange ConfigurationChange { get; set; } - /// Recommended maximum AI credits for this session. - [JsonPropertyName("recommendedCap")] - public double RecommendedCap { 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; } - /// Tier chosen as the recommended cap. - [JsonPropertyName("recommendedTier")] - public SessionLimitPredictionTier RecommendedTier { get; set; } + /// Identity from the retained plan, not caller display text. + [JsonPropertyName("identity")] + public required McpPlanResourceIdentity Identity { get; set; } - /// Baseline fallback level used to create the prediction. - [JsonPropertyName("source")] - public SessionLimitPredictionSource Source { get; set; } + /// Non-secret values supplied for this selected alternative. + [JsonPropertyName("inputs")] + public required IList Inputs { 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; + /// Policy decision bound to this plan. + [JsonPropertyName("policy")] + public required McpPlanPolicyResult Policy { get; set; } - /// Ordered usage tiers and their AI-credit caps. - [JsonPropertyName("tiers")] - public IList Tiers { get => field ??= []; set; } -} + /// Original source identity and content commitment. + [JsonPropertyName("provenance")] + public required McpPlanProvenance Provenance { get; set; } -/// The available variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SessionLimitPredictionResultAvailable : SessionLimitPredictionResult -{ - /// - [JsonIgnore] - public override string Kind => "available"; + /// Explicit reviewed backend selection; no backend is accessed when no secrets are supplied. + [JsonPropertyName("secretStorage")] + public required McpInstallationSecretStorage SecretStorage { get; set; } - /// Predicted session limit details. - [JsonPropertyName("prediction")] - public required SessionLimitPredictionDetails Prediction { 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 unavailable variant of . +/// The uninstall variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class SessionLimitPredictionResultUnavailable : SessionLimitPredictionResult +public partial class McpInstallationReviewUninstall : McpInstallationReview { /// [JsonIgnore] - public override string Kind => "unavailable"; + public override string Action => "uninstall"; - /// Reason no prediction is available. - [JsonPropertyName("reason")] - public required SessionLimitPredictionUnavailableReason Reason { get; set; } -} + /// Identity from the installed receipt. + [JsonPropertyName("identity")] + public required McpPlanResourceIdentity Identity { get; set; } -/// RPC data type for SessionLimitPredictionPredict operations. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionLimitPredictionPredictRequest -{ - /// Client type to size for. Defaults to `cli-interactive`. - [JsonPropertyName("clientType")] - public SessionLimitPredictionClientType? ClientType { get; set; } + /// Receipt-owned installation being removed. + [JsonPropertyName("installationId")] + public required string InstallationId { get; set; } - /// Optional model identifier override. If omitted, the session's current model is used. - [JsonPropertyName("modelId")] - public string? ModelId { get; set; } -} + /// Exact planner-owned secret slots to remove, excluding shared OAuth grants. + [JsonPropertyName("ownedSecretCount")] + public required long OwnedSecretCount { 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; } + /// Current removal policy, independent of permission to activate the server. + [JsonPropertyName("policy")] + public required McpPlanPolicyResult Policy { get; set; } - /// Optional model identifier override. If omitted, the session's current model is used. - [JsonPropertyName("modelId")] - public string? ModelId { get; set; } + /// Shared profile authentication is deliberately retained, not pending cleanup. + [JsonPropertyName("preservesSharedAuthentication")] + public required bool PreservesSharedAuthentication { get; set; } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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; } } -/// GitHub URL for the session and a flag indicating whether remote steering is enabled. +/// The mcp variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class RemoteEnableResult +public partial class InstallationReviewMcp : InstallationReview { - /// Whether remote steering is enabled. - [JsonPropertyName("remoteSteerable")] - public bool RemoteSteerable { get; set; } + /// + [JsonIgnore] + public override string Resource => "mcp"; - /// GitHub frontend URL for this session. - [Url] - [StringSyntax(StringSyntaxAttribute.Uri)] - [JsonPropertyName("url")] - public string? Url { get; set; } + /// The exact MCP action and its reviewed changes. + [JsonPropertyName("review")] + public required McpInstallationReview Review { get; set; } } -/// Optional remote session mode ("off", "export", or "on"); defaults to enabling both export and remote steering. +/// The skill variant of . [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class RemoteEnableRequest +public partial class InstallationReviewSkill : InstallationReview { - /// 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; } + /// + [JsonIgnore] + public override string Resource => "skill"; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// The exact verified Skill action and its reviewed files. + [JsonPropertyName("review")] + public required SkillInstallationReview Review { get; set; } } -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionRemoteDisableRequest +/// One connection-owned, expiring request for a trusted host's explicit user decision. +public sealed class InstallationsConfirmRequest { - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Opaque one-use challenge. Return unchanged; never log or persist. + [JsonPropertyName("confirmationId")] + public string ConfirmationId { 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. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class RemoteNotifySteerableChangedResult -{ -} + /// Original plan expiry as an ISO 8601 timestamp. Confirmation never extends it. + [JsonPropertyName("expiresAt")] + public string ExpiresAt { get; set; } = string.Empty; -/// 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; } + /// Random identifier of this installation operation, not a plan handle. + [JsonPropertyName("operationId")] + public string OperationId { get; set; } = string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { 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; } -/// Current sharing status and shareable GitHub URL for a session. +/// Closed set of public task kinds a connection can negotiate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class VisibilityGetResult +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct TaskKind : IEquatable { - /// 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; } + private readonly string? _value; - /// 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; } + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public TaskKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// 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; } -} + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; -/// 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; -} + /// Runtime-owned background agent task. + public static TaskKind Agent { get; } = new("agent"); -/// Effective sharing status and shareable GitHub URL after updating session visibility. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class VisibilitySetResult -{ - /// 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; } + /// Runtime-owned shell task. + public static TaskKind Shell { get; } = new("shell"); - /// 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; } + /// Client-owned externally executed task. + public static TaskKind Client { get; } = new("client"); - /// 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; } -} + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskKind left, TaskKind right) => left.Equals(right); -/// Desired sharing status for the session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class VisibilitySetRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskKind left, TaskKind right) => !(left == right); - /// 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; } + /// + public override bool Equals(object? obj) => obj is TaskKind other && Equals(other); + + /// + public bool Equals(TaskKind 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 TaskKind 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) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskKind)); + } + } } -/// Scheduled prompt entry with ID, timing (`intervalMs`, `cron`, or `at`), prompt text, recurrence, and next run time. + +/// GitHub Mission Control compute kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ScheduleEntry +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct EnvironmentKind : IEquatable { - /// 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; } + private readonly string? _value; - /// 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; } + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public EnvironmentKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Sequential id assigned by the runtime within the session. Stable across resumes (rebuilt from the event log). - [JsonPropertyName("id")] - public long Id { get; set; } + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Interval between scheduled ticks, in milliseconds (relative-interval schedules). - [JsonConverter(typeof(MillisecondsTimeSpanConverter))] - [JsonPropertyName("intervalMs")] - public TimeSpan? Interval { get; set; } + /// A user-managed environment on a local machine. + public static EnvironmentKind UserLocal { get; } = new("user-local"); - /// ISO 8601 timestamp when the next tick is scheduled to fire. - [JsonPropertyName("nextRunAt")] - public DateTimeOffset NextRunAt { get; set; } + /// A user-managed environment in a GitHub Codespace. + public static EnvironmentKind UserCodespace { get; } = new("user-codespace"); - /// Prompt text that gets enqueued on every tick. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// A GitHub-managed environment backed by GitHub Actions. + public static EnvironmentKind ManagedActions { get; } = new("managed-actions"); - /// Whether the schedule re-arms after each tick (`/every`) or fires once (`/after`). - [JsonPropertyName("recurring")] - public bool Recurring { get; set; } + /// A GitHub-managed sandbox environment. + public static EnvironmentKind ManagedSandbox { get; } = new("managed-sandbox"); - /// 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; } + /// A GitHub-managed cloud coding agent environment. + public static EnvironmentKind ManagedCca { get; } = new("managed-cca"); - /// IANA timezone the `cron` expression is evaluated in. - [JsonPropertyName("tz")] - public string? Tz { get; set; } -} + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(EnvironmentKind left, EnvironmentKind right) => left.Equals(right); -/// Snapshot of the currently active recurring prompts for this session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ScheduleList -{ - /// Active scheduled prompts, ordered by id. - [JsonPropertyName("entries")] - public IList Entries { get => field ??= []; set; } -} + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(EnvironmentKind left, EnvironmentKind right) => !(left == right); -/// 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; -} + /// + public override bool Equals(object? obj) => obj is EnvironmentKind other && Equals(other); -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionScheduleHydrateRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + public bool Equals(EnvironmentKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); -/// Whether the session currently has an active self-paced schedule. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ScheduleHasSelfPacedResult -{ - /// True when at least one active schedule is self-paced. - [JsonPropertyName("hasSelfPaced")] - public bool HasSelfPaced { get; set; } -} + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); -/// Identifies the target session. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class SessionScheduleHasSelfPacedRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + public override string ToString() => Value; -/// 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; } + /// 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)); + } - /// User-facing validation error, when registration failed. - [JsonPropertyName("error")] - public string? Error { get; set; } + /// + public override void Write(Utf8JsonWriter writer, EnvironmentKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EnvironmentKind)); + } + } } -/// Register a relative-interval scheduled prompt. + +/// Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ScheduleAddRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct HookType : IEquatable { - /// Optional display-only prompt label. - [JsonPropertyName("displayPrompt")] - public string? DisplayPrompt { get; set; } + private readonly string? _value; - /// Human-readable interval such as `30s`, `5m`, or `2h`. - [JsonPropertyName("interval")] - public string Interval { get; set; } = string.Empty; + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public HookType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Prompt text to enqueue when the schedule fires. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Whether the schedule should re-arm after each tick. Defaults to true. - [JsonPropertyName("recurring")] - public bool? Recurring { get; set; } + /// Runs before a tool is invoked. + public static HookType PreToolUse { get; } = new("preToolUse"); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Runs before an MCP tool is invoked. + public static HookType PreMcpToolCall { get; } = new("preMcpToolCall"); -/// Register a cron scheduled prompt. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -internal sealed class ScheduleAddCronRequest -{ - /// 5-field cron expression. - [JsonPropertyName("cron")] - public string Cron { get; set; } = string.Empty; + /// Runs after a tool completes successfully. + public static HookType PostToolUse { get; } = new("postToolUse"); - /// Optional display-only prompt label. - [JsonPropertyName("displayPrompt")] - public string? DisplayPrompt { get; set; } + /// Runs after a tool fails. + public static HookType PostToolUseFailure { get; } = new("postToolUseFailure"); - /// Prompt text to enqueue when the schedule fires. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Runs after the user submits a prompt. + public static HookType UserPromptSubmitted { get; } = new("userPromptSubmitted"); - /// Whether the schedule should re-arm after each tick. Defaults to true. - [JsonPropertyName("recurring")] - public bool? Recurring { get; set; } + /// 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"); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Runs when a session starts. + public static HookType SessionStart { get; } = new("sessionStart"); - /// IANA timezone for evaluating the cron expression. - [JsonPropertyName("tz")] - public string? Tz { get; set; } -} + /// Runs when a session ends. + public static HookType SessionEnd { get; } = new("sessionEnd"); -/// 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; } + /// Runs after an agent result is produced. + public static HookType PostResult { get; } = new("postResult"); - /// Optional display-only prompt label. - [JsonPropertyName("displayPrompt")] - public string? DisplayPrompt { get; set; } + /// Runs before a pull request description is generated. + public static HookType PrePRDescription { get; } = new("prePRDescription"); - /// Prompt text to enqueue when the schedule fires. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Runs when the agent encounters an error. + public static HookType ErrorOccurred { get; } = new("errorOccurred"); - /// Whether the schedule should re-arm after each tick. Defaults to false. - [JsonPropertyName("recurring")] - public bool? Recurring { get; set; } + /// Runs when the agent stops. + public static HookType AgentStop { get; } = new("agentStop"); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Runs when a subagent starts. + public static HookType SubagentStart { get; } = new("subagentStart"); -/// 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; } + /// Runs when a subagent stops. + public static HookType SubagentStop { get; } = new("subagentStop"); - /// Prompt text to enqueue when the schedule fires. - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; + /// Runs before conversation context is compacted. + public static HookType PreCompact { get; } = new("preCompact"); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Runs when the agent requests permission. + public static HookType PermissionRequest { get; } = new("permissionRequest"); -/// 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; } + /// Runs when the agent emits a notification. + public static HookType Notification { get; } = new("notification"); - /// Id of the self-paced scheduled prompt. - [JsonPropertyName("id")] - public long Id { get; set; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(HookType left, HookType right) => left.Equals(right); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(HookType left, HookType right) => !(left == right); -/// 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; } -} + /// + public override bool Equals(object? obj) => obj is HookType other && Equals(other); -/// 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; } + /// + public bool Equals(HookType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); -/// 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; -} + /// + public override string ToString() => Value; -/// 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; + /// 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)); + } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + public override void Write(Utf8JsonWriter writer, HookType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HookType)); + } + } } -/// 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. +/// Configuration tier that contributed a discovered hook action. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class WorkflowExecuteRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct HookOrigin : IEquatable { - /// Workflow input value. - [JsonPropertyName("args")] - public JsonElement Args { get; set; } + private readonly string? _value; - /// Opaque token identifying this workflow execution attempt. - [JsonPropertyName("executionToken")] - public string ExecutionToken { get; set; } = string.Empty; + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public HookOrigin(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Registered workflow name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Hook loaded from user settings or the user's hook directory. + public static HookOrigin User { get; } = new("user"); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Hook loaded from repository settings or the repository hook directory. + public static HookOrigin Repository { get; } = new("repository"); -/// 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; + /// 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"); - /// Workflow run identifier. - [JsonPropertyName("runId")] - public string RunId { get; set; } = string.Empty; + /// Hook enforced by centrally managed policy. + public static HookOrigin Policy { get; } = new("policy"); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(HookOrigin left, HookOrigin right) => left.Equals(right); -/// 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; } -} + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(HookOrigin left, HookOrigin right) => !(left == right); -/// 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; + /// + public override bool Equals(object? obj) => obj is HookOrigin other && Equals(other); - /// Owner-scoped task key included for correlation. - [JsonPropertyName("clientTaskId")] - public string ClientTaskId { get; set; } = string.Empty; + /// + public bool Equals(HookOrigin other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Canonical runtime-generated task identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Reason the runtime requests cancellation. - [JsonPropertyName("reason")] - public ClientTaskCancelReason Reason { get; set; } + /// + public override string ToString() => Value; - /// Session that owns the client task. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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)); + } + } } -/// Describes a filesystem error. + +/// Resolved Anthropic adaptive-thinking capability for a model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsError +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AdaptiveThinkingSupport : IEquatable { - /// Error classification. - [JsonPropertyName("code")] - public SessionFsErrorCode Code { get; set; } + private readonly string? _value; - /// Free-form detail about the error, for logging/diagnostics. - [JsonPropertyName("message")] - public string? Message { get; set; } -} + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AdaptiveThinkingSupport(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } -/// 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; + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Describes a filesystem error. - [JsonPropertyName("error")] - public SessionFsError? Error { get; set; } -} + /// The model does not accept thinking.type='adaptive'. + public static AdaptiveThinkingSupport Unsupported { get; } = new("unsupported"); -/// 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; + /// The model accepts adaptive thinking but also accepts thinking.type='enabled'. + public static AdaptiveThinkingSupport Optional { get; } = new("optional"); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// 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"); -/// 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; + /// 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"); - /// Optional POSIX-style mode for newly created files. - [JsonPropertyName("mode")] - public long? Mode { get; set; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AdaptiveThinkingSupport left, AdaptiveThinkingSupport right) => left.Equals(right); - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AdaptiveThinkingSupport left, AdaptiveThinkingSupport right) => !(left == right); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + public override bool Equals(object? obj) => obj is AdaptiveThinkingSupport other && Equals(other); -/// 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; + /// + public bool Equals(AdaptiveThinkingSupport other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Optional POSIX-style mode for newly created files. - [JsonPropertyName("mode")] - public long? Mode { get; set; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// + public override string ToString() => Value; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// 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)); + } -/// 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; } + /// + public override void Write(Utf8JsonWriter writer, AdaptiveThinkingSupport value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AdaptiveThinkingSupport)); + } + } } -/// Path to test for existence in the client-provided session filesystem. + +/// Model capability category for grouping in the model picker. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsExistsRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelPickerCategory : IEquatable { - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + private readonly string? _value; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelPickerCategory(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } -/// 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; } + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Describes a filesystem error. - [JsonPropertyName("error")] - public SessionFsError? Error { get; set; } + /// Lightweight model category optimized for faster, lower-cost interactions. + public static ModelPickerCategory Lightweight { get; } = new("lightweight"); - /// Whether the path is a directory. - [JsonPropertyName("isDirectory")] - public bool IsDirectory { get; set; } + /// Versatile model category suitable for a broad range of tasks. + public static ModelPickerCategory Versatile { get; } = new("versatile"); - /// Whether the path is a file. - [JsonPropertyName("isFile")] - public bool IsFile { get; set; } + /// Powerful model category optimized for complex tasks. + public static ModelPickerCategory Powerful { get; } = new("powerful"); - /// ISO 8601 timestamp of last modification. - [JsonPropertyName("mtime")] - public DateTimeOffset Mtime { get; set; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelPickerCategory left, ModelPickerCategory right) => left.Equals(right); - /// File size in bytes. - [JsonPropertyName("size")] - public long Size { get; set; } -} + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelPickerCategory left, ModelPickerCategory right) => !(left == right); -/// 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; + /// + public override bool Equals(object? obj) => obj is ModelPickerCategory other && Equals(other); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + public bool Equals(ModelPickerCategory other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); -/// 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; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// + public override string ToString() => Value; - /// Create parent directories as needed. - [JsonPropertyName("recursive")] - public bool? Recursive { get; set; } + /// 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)); + } - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// + public override void Write(Utf8JsonWriter writer, ModelPickerCategory value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelPickerCategory)); + } + } } -/// Names of entries in the requested directory, or a filesystem error if the read failed. + +/// Relative cost tier for token-based billing users. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsReaddirResult +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelPickerPriceCategory : IEquatable { - /// Entry names in the directory. - [JsonPropertyName("entries")] - public IList Entries { get => field ??= []; set; } + private readonly string? _value; - /// Describes a filesystem error. - [JsonPropertyName("error")] - public SessionFsError? Error { get; set; } -} + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelPickerPriceCategory(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } -/// 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; + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Lowest relative token cost tier. + public static ModelPickerPriceCategory Low { get; } = new("low"); -/// 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; + /// Medium relative token cost tier. + public static ModelPickerPriceCategory Medium { get; } = new("medium"); - /// Entry type. - [JsonPropertyName("type")] - public SessionFsReaddirWithTypesEntryType Type { get; set; } -} + /// High relative token cost tier. + public static ModelPickerPriceCategory High { get; } = new("high"); -/// 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; } + /// Highest relative token cost tier. + public static ModelPickerPriceCategory VeryHigh { get; } = new("very_high"); - /// Describes a filesystem error. - [JsonPropertyName("error")] - public SessionFsError? Error { get; set; } -} + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelPickerPriceCategory left, ModelPickerPriceCategory right) => left.Equals(right); -/// 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; + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelPickerPriceCategory left, ModelPickerPriceCategory right) => !(left == right); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + public override bool Equals(object? obj) => obj is ModelPickerPriceCategory other && Equals(other); -/// 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; } + /// + public bool Equals(ModelPickerPriceCategory other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Path using SessionFs conventions. - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Remove directories and their contents recursively. - [JsonPropertyName("recursive")] - public bool? Recursive { get; set; } + /// + public override string ToString() => Value; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// 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)); + } + } } -/// Source and destination paths for renaming or moving an entry in the client-provided session filesystem. + +/// Current policy state for this model. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class SessionFsRenameRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelPolicyState : IEquatable { - /// Destination path using SessionFs conventions. - [JsonPropertyName("dest")] - public string Dest { get; set; } = string.Empty; + private readonly string? _value; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelPolicyState(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Source path using SessionFs conventions. - [JsonPropertyName("src")] - public string Src { get; set; } = string.Empty; -} + /// Gets the value associated with this . + public string Value => _value ?? 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; } + /// The model is enabled by policy. + public static ModelPolicyState Enabled { get; } = new("enabled"); - /// Describes a filesystem error. - [JsonPropertyName("error")] - public SessionFsError? Error { get; set; } + /// The model is disabled by policy. + public static ModelPolicyState Disabled { get; } = new("disabled"); - /// SQLite last_insert_rowid() value for INSERT. - [JsonPropertyName("lastInsertRowid")] - public long? LastInsertRowid { get; set; } + /// No explicit policy is configured for the model. + public static ModelPolicyState Unconfigured { get; } = new("unconfigured"); - /// For SELECT: array of row objects. For others: empty array. - [JsonPropertyName("rows")] - public IList> Rows { get => field ??= []; set; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelPolicyState left, ModelPolicyState right) => left.Equals(right); - /// Number of rows affected (for INSERT/UPDATE/DELETE). - [JsonPropertyName("rowsAffected")] - public long RowsAffected { get; set; } -} + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelPolicyState left, ModelPolicyState right) => !(left == right); -/// 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; } + /// + public override bool Equals(object? obj) => obj is ModelPolicyState other && Equals(other); - /// SQL query to execute. - [JsonPropertyName("query")] - public string Query { get; set; } = string.Empty; + /// + public bool Equals(ModelPolicyState other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// 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; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + public override string ToString() => Value; -/// 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; } + /// 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)); + } - /// Human-readable transaction failure message. - [JsonPropertyName("message")] - public string Message { get; set; } = string.Empty; + /// + public override void Write(Utf8JsonWriter writer, ModelPolicyState value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelPolicyState)); + } + } } -/// Per-statement results, or a classified transaction error. + +/// 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)] -public sealed class SessionFsSqliteTransactionResult +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelProviderKind : IEquatable { - /// Classified transaction failure, when execution did not succeed. - [JsonPropertyName("error")] - public SessionFsSqliteTransactionError? Error { get; set; } + private readonly string? _value; - /// Per-statement query results in input order. - [JsonPropertyName("results")] - public IList Results { get => field ??= []; set; } -} + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelProviderKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } -/// 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; } + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// SQL statement to execute. - [JsonPropertyName("query")] - public string Query { get; set; } = string.Empty; + /// GitHub Copilot / CAPI models, spawned by a github-resolving account that holds a Copilot seat. + public static ModelProviderKind Copilot { get; } = new("copilot"); - /// How to execute the statement. - [JsonPropertyName("queryType")] - public SessionFsSqliteQueryType QueryType { get; set; } -} + /// Microsoft 365 Copilot (Loki) inference models, spawned by a resolvable Entra-derived Loki account. + public static ModelProviderKind Loki { get; } = new("loki"); -/// 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; + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelProviderKind left, ModelProviderKind right) => left.Equals(right); - /// Ordered SQL statements to execute in one transaction. - [JsonPropertyName("statements")] - public IList Statements { get => field ??= []; set; } -} + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelProviderKind left, ModelProviderKind right) => !(left == right); -/// 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; } -} + /// + public override bool Equals(object? obj) => obj is ModelProviderKind other && Equals(other); -/// Identifies the target session. -public sealed class SessionFsSqliteExistsRequest -{ - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// + public bool Equals(ModelProviderKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); -/// 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; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Provider-supplied title. - [JsonPropertyName("title")] - public string? Title { get; set; } + /// + public override string ToString() => Value; - /// URL for web-rendered canvases. - [JsonPropertyName("url")] - public string? Url { get; set; } -} + /// 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)); + } -/// Host capabilities. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasHostContextCapabilities -{ - /// Whether canvas rendering is supported. - [JsonPropertyName("canvases")] - public bool? Canvases { get; set; } + /// + public override void Write(Utf8JsonWriter writer, ModelProviderKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderKind)); + } + } } -/// 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. +/// State of the persistent certificate authority of the sandbox credential proxy. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasSessionContext +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct SandboxProxyCaState : IEquatable { - /// Active session working directory, when known. - [JsonPropertyName("workingDirectory")] - public string? WorkingDirectory { get; set; } -} + private readonly string? _value; -/// 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; + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public SandboxProxyCaState(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Owning provider identifier. - [JsonPropertyName("extensionId")] - public string ExtensionId { get; set; } = string.Empty; + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Host context supplied by the runtime. - [JsonPropertyName("host")] - public CanvasHostContext? Host { get; set; } + /// This platform has no supported OS trust store. The proxy uses a per-process certificate bundle. + public static SandboxProxyCaState Unsupported { get; } = new("unsupported"); - /// Canvas open input. - [JsonPropertyName("input")] - public JsonElement? Input { get; set; } + /// OS trust does not include the certificate authority, or none is stored. + public static SandboxProxyCaState NotInstalled { get; } = new("notInstalled"); - /// Stable caller-supplied canvas instance identifier. - [JsonPropertyName("instanceId")] - public string InstanceId { get; set; } = string.Empty; + /// OS trust includes the stored certificate authority. + public static SandboxProxyCaState Installed { get; } = new("installed"); - /// Session context supplied by the runtime. - [JsonPropertyName("session")] - public CanvasSessionContext? Session { get; set; } + /// The runtime could not read the certificate authority or the OS trust store. + public static SandboxProxyCaState Error { get; } = new("error"); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(SandboxProxyCaState left, SandboxProxyCaState right) => left.Equals(right); -/// 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; + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(SandboxProxyCaState left, SandboxProxyCaState right) => !(left == right); - /// Owning provider identifier. - [JsonPropertyName("extensionId")] - public string ExtensionId { get; set; } = string.Empty; + /// + public override bool Equals(object? obj) => obj is SandboxProxyCaState other && Equals(other); - /// Host context supplied by the runtime. - [JsonPropertyName("host")] - public CanvasHostContext? Host { get; set; } + /// + public bool Equals(SandboxProxyCaState other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Canvas instance identifier. - [JsonPropertyName("instanceId")] - public string InstanceId { get; set; } = string.Empty; + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Session context supplied by the runtime. - [JsonPropertyName("session")] - public CanvasSessionContext? Session { get; set; } + /// + public override string ToString() => Value; - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override SandboxProxyCaState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, SandboxProxyCaState value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SandboxProxyCaState)); + } + } } -/// Canvas action invocation parameters sent to the provider. + +/// Server transport type: stdio, http, sse (deprecated), or memory. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class CanvasProviderInvokeActionRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct DiscoveredMcpServerType : IEquatable { - /// Action name to invoke. - [JsonPropertyName("actionName")] - public string ActionName { get; set; } = string.Empty; + private readonly string? _value; - /// Provider-local canvas identifier. - [JsonPropertyName("canvasId")] - public string CanvasId { get; set; } = string.Empty; + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public DiscoveredMcpServerType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Owning provider identifier. - [JsonPropertyName("extensionId")] - public string ExtensionId { get; set; } = string.Empty; + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Host context supplied by the runtime. - [JsonPropertyName("host")] - public CanvasHostContext? Host { get; set; } + /// Server communicates over stdio with a local child process. + public static DiscoveredMcpServerType Stdio { get; } = new("stdio"); - /// Action input. - [JsonPropertyName("input")] - public JsonElement? Input { get; set; } + /// Server communicates over streamable HTTP. + public static DiscoveredMcpServerType Http { get; } = new("http"); - /// Canvas instance identifier. - [JsonPropertyName("instanceId")] - public string InstanceId { get; set; } = string.Empty; + /// Server communicates over Server-Sent Events (deprecated). + public static DiscoveredMcpServerType Sse { get; } = new("sse"); - /// Session context supplied by the runtime. - [JsonPropertyName("session")] - public CanvasSessionContext? Session { get; set; } + /// Server is backed by an in-memory runtime implementation. + public static DiscoveredMcpServerType Memory { get; } = new("memory"); - /// Target session identifier. - [JsonPropertyName("sessionId")] - public string SessionId { get; set; } = string.Empty; -} + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(DiscoveredMcpServerType left, DiscoveredMcpServerType right) => left.Equals(right); -/// 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; } + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(DiscoveredMcpServerType left, DiscoveredMcpServerType right) => !(left == right); - /// Process exit status when available; absent for in-process listener tasks. - [JsonPropertyName("exitCode")] - public long? ExitCode { get; set; } + /// + public override bool Equals(object? obj) => obj is DiscoveredMcpServerType other && Equals(other); - /// Listener UUID. - [JsonPropertyName("hostId")] - public string HostId { get; set; } = string.Empty; + /// + public bool Equals(DiscoveredMcpServerType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Cause of termination. - [JsonPropertyName("reason")] - public HostExitReason Reason { get; set; } -} + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); -/// 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; } + /// + public override string ToString() => Value; - /// 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; } + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override DiscoveredMcpServerType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } - /// 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; + /// + public override void Write(Utf8JsonWriter writer, DiscoveredMcpServerType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredMcpServerType)); + } + } } -/// 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. +/// 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)] -public sealed class ExtensionLaunchProviderResolveRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct CatalogCapability : IEquatable { - /// Source-qualified extension identifier. - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; + private readonly string? _value; - /// Absolute path to the discovered extension entrypoint. - [JsonPropertyName("modulePath")] - public string ModulePath { get; set; } = string.Empty; + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public CatalogCapability(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Human-readable extension name. - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Discovery source for the extension entrypoint. - [JsonPropertyName("source")] - public ExtensionSource Source { get; set; } -} + /// Understands the current `application/mcp-server-card+json` media type. + public static CatalogCapability McpServerCard { get; } = new("mcp-server-card"); -/// 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 -{ -} + /// Understands the legacy `application/mcp-server+json` media type. + public static CatalogCapability LegacyMcpServerCard { get; } = new("legacy-mcp-server-card"); -/// 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; } + /// Understands `application/ai-skill` candidates as discovery-only and typed non-installable. + public static CatalogCapability AiSkillDiscovery { get; } = new("ai-skill-discovery"); - /// 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; } + /// 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"); - /// HTTP request headers, preserving multiple values per name. - [JsonPropertyName("headers")] - public IDictionary> Headers { get => field ??= new Dictionary>(); set; } + /// 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"); - /// 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; } + /// Understands plans that enumerate every eligible transport rather than a single preferred one. + public static CatalogCapability MultipleTransportChoice { get; } = new("multiple-transport-choice"); - /// HTTP method, e.g. GET, POST. - [JsonPropertyName("method")] - public string Method { get; set; } = string.Empty; + /// 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"); - /// 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; } + /// 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"); - /// 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; + /// Understands exact candidate selection through model-safe opaque references and host-only candidate-handle hand-off. + public static CatalogCapability CatalogSelection { get; } = new("catalog-selection"); - /// 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; } + /// 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"); - /// 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; } + /// 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"); - /// Absolute request URL. - [JsonPropertyName("url")] - public string Url { get; set; } = string.Empty; -} + /// 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"); -/// 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 -{ -} + /// 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"); -/// 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; } + /// 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"); - /// When true, `data` is base64-encoded bytes. When absent or false, `data` is UTF-8 text. - [JsonPropertyName("binary")] - public bool? Binary { get; set; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CatalogCapability left, CatalogCapability right) => left.Equals(right); - /// 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; } + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CatalogCapability left, CatalogCapability right) => !(left == right); - /// Optional human-readable reason for the cancellation, propagated for logging. - [JsonPropertyName("cancelReason")] - public string? CancelReason { get; set; } + /// + public override bool Equals(object? obj) => obj is CatalogCapability other && Equals(other); - /// 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; + /// + public bool Equals(CatalogCapability other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// 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; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; - /// Matches the requestId from the originating httpRequestStart frame. - [JsonPropertyName("requestId")] - public string RequestId { get; set; } = string.Empty; + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override CatalogCapability 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) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogCapability)); + } + } } -/// Client environment metadata describing the process that produced a telemetry event. + +/// Whether a planned configuration change would create or modify an entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class GitHubTelemetryClientInfo +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpPlanConfigurationOperation : IEquatable { - /// Copilot CLI version string. - [JsonPropertyName("cli_version")] - public string CliVersion { get; set; } = string.Empty; + private readonly string? _value; - /// Name of the client application. - [JsonPropertyName("client_name")] - public string? ClientName { get; set; } + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpPlanConfigurationOperation(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Type of client. - [JsonPropertyName("client_type")] - public string? ClientType { get; set; } + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Copilot subscription plan, when known. - [JsonPropertyName("copilot_plan")] - public string? CopilotPlan { get; set; } + /// Creates a configuration entry that does not exist yet. + public static McpPlanConfigurationOperation Add { get; } = new("add"); - /// Number of logical CPU cores on the host. - [JsonPropertyName("cpu_count")] - public long? CpuCount { get; set; } + /// Modifies a configuration entry that already exists. + public static McpPlanConfigurationOperation Update { get; } = new("update"); - /// Distinct CPU model names for the host, comma-separated. - [JsonPropertyName("cpu_model")] - public string? CpuModel { get; set; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanConfigurationOperation left, McpPlanConfigurationOperation right) => left.Equals(right); - /// Stable machine identifier for the device. - [JsonPropertyName("dev_device_id")] - public string? DevDeviceId { get; set; } + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanConfigurationOperation left, McpPlanConfigurationOperation right) => !(left == right); - /// Whether the user is a GitHub/Microsoft staff member. - [JsonPropertyName("is_staff")] - public bool? IsStaff { get; set; } + /// + public override bool Equals(object? obj) => obj is McpPlanConfigurationOperation other && Equals(other); - /// Node.js runtime version string. - [JsonPropertyName("node_version")] - public string NodeVersion { get; set; } = string.Empty; + /// + public bool Equals(McpPlanConfigurationOperation other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Operating system architecture (e.g. arm64, x64). - [JsonPropertyName("os_arch")] - public string OsArch { get; set; } = string.Empty; + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Operating system platform (e.g. darwin, linux, win32). - [JsonPropertyName("os_platform")] - public string OsPlatform { get; set; } = string.Empty; + /// + public override string ToString() => Value; - /// Operating system version string. - [JsonPropertyName("os_version")] - public string OsVersion { get; set; } = string.Empty; + /// 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)); + } + } } -/// 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. + +/// Configuration scope an MCP install plan targets. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class GitHubTelemetryEvent +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpPlanScope : IEquatable { - /// Client environment metadata. - [JsonPropertyName("client")] - public GitHubTelemetryClientInfo? Client { get; set; } + private readonly string? _value; - /// Copilot tracking ID for user-level attribution. - [JsonPropertyName("copilot_tracking_id")] - public string? CopilotTrackingId { get; set; } + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpPlanScope(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Timestamp when the event was created (ISO 8601 format). - [JsonPropertyName("created_at")] - public string? CreatedAt { get; set; } + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Experiment assignment context. - [JsonPropertyName("exp_assignment_context")] - public string? ExpAssignmentContext { get; set; } + /// The user's own MCP configuration. + public static McpPlanScope User { get; } = new("user"); - /// Feature flags enabled for this session, as a map from flag to value. - [JsonPropertyName("features")] - public IDictionary? Features { get; set; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanScope left, McpPlanScope right) => left.Equals(right); - /// Event type/kind (e.g. get_completion_with_tools_turn, tool_call_executed). - [JsonPropertyName("kind")] - public string Kind { get; set; } = string.Empty; + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanScope left, McpPlanScope right) => !(left == right); - /// Numeric metrics as a map from key to value. - [JsonPropertyName("metrics")] - public IDictionary Metrics { get => field ??= new Dictionary(); set; } + /// + public override bool Equals(object? obj) => obj is McpPlanScope other && Equals(other); - /// Reference to the model call that produced this event. - [JsonPropertyName("model_call_id")] - public string? ModelCallId { get; set; } + /// + public bool Equals(McpPlanScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// String-valued properties as a map from key to value. - [JsonPropertyName("properties")] - public IDictionary Properties { get => field ??= new Dictionary(); set; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Session identifier the event belongs to. - [JsonPropertyName("session_id")] - public string? SessionId { get; set; } + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override McpPlanScope 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) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanScope)); + } + } } -/// 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. + +/// What policy decided for a planned server. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class GitHubTelemetryNotification +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpPlanPolicyDecision : IEquatable { - /// The telemetry event, in the runtime's native GitHub-shaped telemetry format. - [JsonPropertyName("event")] - public GitHubTelemetryEvent Event { get => field ??= new(); set; } + private readonly string? _value; - /// 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; } + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpPlanPolicyDecision(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// 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; } -} + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; -/// 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; -} + /// 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 token variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class GitHubTokenAcquireResultToken : GitHubTokenAcquireResult -{ - /// - [JsonIgnore] - public override string Kind => "token"; + /// Policy permits the server only after an explicit approval. + public static McpPlanPolicyDecision RequiresApproval { get; } = new("requires-approval"); - /// GitHub access token acquired by the SDK host. - [JsonPropertyName("accessToken")] - public required string AccessToken { get; set; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanPolicyDecision left, McpPlanPolicyDecision right) => left.Equals(right); - /// 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; } + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanPolicyDecision left, McpPlanPolicyDecision right) => !(left == right); - /// OAuth token type. Defaults to bearer when omitted. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("tokenType")] - public string? TokenType { get; set; } -} + /// + public override bool Equals(object? obj) => obj is McpPlanPolicyDecision other && Equals(other); -/// The cancelled variant of . -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class GitHubTokenAcquireResultCancelled : GitHubTokenAcquireResult -{ /// - [JsonIgnore] - public override string Kind => "cancelled"; + public bool Equals(McpPlanPolicyDecision 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 McpPlanPolicyDecision 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) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPolicyDecision)); + } + } } -/// Asks the SDK client to acquire a GitHub access token from an opaque callback registration. + +/// Which authority produced a policy decision. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class GitHubTokenAcquireRequest +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpPlanPolicySource : IEquatable { - /// Effective GitHub host for which the callback must return a token. - [JsonPropertyName("host")] - public string Host { get; set; } = string.Empty; + private readonly string? _value; - /// Why the runtime is requesting a GitHub credential. - [JsonPropertyName("reason")] - public GitHubTokenAcquireReason Reason { get; set; } + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpPlanPolicySource(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Opaque identifier generated by the SDK for this callback registration. - [JsonPropertyName("registrationId")] - public string RegistrationId { get; set; } = string.Empty; + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Session receiving the token. Absent only before a cloud session has been assigned its id. - [JsonPropertyName("sessionId")] - public string? SessionId { get; set; } -} + /// No policy applied, so the server is permitted by default. + public static McpPlanPolicySource None { get; } = new("none"); -/// 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; + /// An enterprise allowlist evaluated the server. + public static McpPlanPolicySource EnterpriseAllowlist { get; } = new("enterprise-allowlist"); - /// Fresh explicit user decision. There is no default. - [JsonPropertyName("decision")] - public InstallationDecision Decision { get; set; } + /// The registry the card came from evaluated the server. + public static McpPlanPolicySource RegistryPolicy { get; } = new("registry-policy"); - /// Exact review commitment from the request. - [JsonPropertyName("reviewFingerprint")] - public string ReviewFingerprint { get; set; } = string.Empty; -} + /// Local trust settings evaluated the server. + public static McpPlanPolicySource LocalTrust { get; } = new("local-trust"); -/// 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; -} + /// 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); -/// 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; -} + /// + public override bool Equals(object? obj) => obj is McpPlanPolicySource other && Equals(other); + /// + public bool Equals(McpPlanPolicySource other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); -/// -/// 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; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Configured tool selection, not permission to invoke those tools. - [JsonPropertyName("tools")] - public IList Tools { get => field ??= []; set; } + /// + public override string ToString() => Value; - /// Transport in the effective persisted remote configuration. - [JsonPropertyName("transport")] - public McpPlanRemoteTransport Transport { get; set; } + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override McpPlanPolicySource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } - /// Exact resolved endpoint, without templates or secret placeholders. - [JsonPropertyName("url")] - public string Url { get; set; } = string.Empty; + /// + public override void Write(Utf8JsonWriter writer, McpPlanPolicySource value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPolicySource)); + } + } } -/// The install variant of . + +/// Canonical digest algorithm for a validated MCP card. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpInstallationReviewInstall : McpInstallationReview +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct CardDigestAlgorithm : IEquatable { - /// - [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; } + private readonly string? _value; - /// Original catalogue trust metadata, not a verification claim. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("catalogueTrust")] - public CatalogTrustSnapshot? CatalogueTrust { get; set; } + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public CardDigestAlgorithm(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// The configuration change for the selected alternative only. - [JsonPropertyName("configurationChange")] - public required McpPlanConfigurationChange ConfigurationChange { get; set; } + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// - /// 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; } + /// SHA-256 over RFC 8785 canonical JSON encoded as UTF-8. + public static CardDigestAlgorithm Sha256Rfc8785 { get; } = new("sha256-rfc8785"); - /// 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 equivalent. + public static bool operator ==(CardDigestAlgorithm left, CardDigestAlgorithm right) => left.Equals(right); - /// Non-secret values supplied for this selected alternative. - [JsonPropertyName("inputs")] - public required IList Inputs { get; set; } + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CardDigestAlgorithm left, CardDigestAlgorithm right) => !(left == right); - /// Policy decision bound to this plan. - [JsonPropertyName("policy")] - public required McpPlanPolicyResult Policy { get; set; } + /// + public override bool Equals(object? obj) => obj is CardDigestAlgorithm other && Equals(other); - /// Original source identity and content commitment. - [JsonPropertyName("provenance")] - public required McpPlanProvenance Provenance { get; set; } + /// + public bool Equals(CardDigestAlgorithm other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Explicit reviewed backend selection; no backend is accessed when no secrets are supplied. - [JsonPropertyName("secretStorage")] - public required McpInstallationSecretStorage SecretStorage { get; set; } + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Only the selected alternative is applied. - [JsonPropertyName("selectedChoice")] - public required McpPlanTransportChoice SelectedChoice { get; set; } + /// + public override string ToString() => Value; - /// Exact reviewed placeholders supplied separately. Never secret values. - [JsonPropertyName("suppliedSecrets")] - public required IList SuppliedSecrets { get; set; } + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override CardDigestAlgorithm Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } - /// Exact reviewed user-scope destination. - [JsonPropertyName("target")] - public required McpPlanTarget Target { get; set; } + /// + public override void Write(Utf8JsonWriter writer, CardDigestAlgorithm value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CardDigestAlgorithm)); + } + } } -/// The uninstall variant of . + +/// JSON MCP card media type accepted for install planning. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public partial class McpInstallationReviewUninstall : McpInstallationReview +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpServerCardMediaType : IEquatable { - /// - [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; } + private readonly string? _value; - /// Exact planner-owned secret slots to remove, excluding shared OAuth grants. - [JsonPropertyName("ownedSecretCount")] - public required long OwnedSecretCount { get; set; } + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpServerCardMediaType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Current removal policy, independent of permission to activate the server. - [JsonPropertyName("policy")] - public required McpPlanPolicyResult Policy { get; set; } + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Shared profile authentication is deliberately retained, not pending cleanup. - [JsonPropertyName("preservesSharedAuthentication")] - public required bool PreservesSharedAuthentication { get; set; } + /// The current MCP server card media type. + public static McpServerCardMediaType ApplicationMcpServerCardJson { get; } = new("application/mcp-server-card+json"); - /// Source identity and content commitment retained by the installed receipt. - [JsonPropertyName("provenance")] - public required McpPlanProvenance Provenance { get; set; } + /// The legacy MCP server card media type, accepted for compatibility. + public static McpServerCardMediaType ApplicationMcpServerJson { get; } = new("application/mcp-server+json"); - /// Whether uninstall restores a protected pre-install configuration. - [JsonPropertyName("restoresPreviousConfiguration")] - public required bool RestoresPreviousConfiguration { get; set; } + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpServerCardMediaType left, McpServerCardMediaType right) => left.Equals(right); - /// Exact destination, checked for intervening changes before mutation. - [JsonPropertyName("target")] - public required McpPlanTarget Target { get; set; } -} + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpServerCardMediaType left, McpServerCardMediaType right) => !(left == right); -/// 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; } -} + public override bool Equals(object? obj) => obj is McpServerCardMediaType other && Equals(other); -/// 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; + public bool Equals(McpServerCardMediaType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Random identifier of this installation operation, not a plan handle. - [JsonPropertyName("operationId")] - public string OperationId { get; set; } = string.Empty; + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// - /// 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; } + /// + public override string ToString() => Value; - /// Resource-specific review to present before collecting the user's decision. - [JsonPropertyName("review")] - public InstallationReview Review { get => field ??= new(); set; } + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override McpServerCardMediaType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } - /// Opaque commitment to the exact review and inputs. Return unchanged; never log. - [JsonPropertyName("reviewFingerprint")] - public string ReviewFingerprint { get; set; } = string.Empty; + /// + public override void Write(Utf8JsonWriter writer, McpServerCardMediaType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpServerCardMediaType)); + } + } } -/// Closed set of public task kinds a connection can negotiate. + +/// 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 TaskKind : 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 TaskKind(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; - /// Runtime-owned background agent task. - public static TaskKind Agent { get; } = new("agent"); + /// Set as an environment variable on the launched process. + public static McpPlanValueCategory EnvironmentVariable { get; } = new("environment-variable"); - /// Runtime-owned shell task. - public static TaskKind Shell { get; } = new("shell"); + /// Passed to the runtime that launches the package. + public static McpPlanValueCategory RuntimeArgument { get; } = new("runtime-argument"); - /// Client-owned externally executed task. - public static TaskKind Client { get; } = new("client"); + /// Passed to the packaged server itself. + public static McpPlanValueCategory PackageArgument { get; } = new("package-argument"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskKind left, TaskKind right) => left.Equals(right); + /// Sent as a request header to a remote endpoint. + public static McpPlanValueCategory Header { get; } = new("header"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(TaskKind left, TaskKind right) => !(left == right); + /// 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 TaskKind other && Equals(other); + public override bool Equals(object? obj) => obj is McpPlanValueCategory other && Equals(other); /// - public bool Equals(TaskKind 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); @@ -25479,107 +28106,128 @@ 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 McpPlanValueCategory 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, McpPlanValueCategory value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanValueCategory)); } } } -/// Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. +/// Scalar type a required value must conform to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct HookType : 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 HookType(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; - /// Runs before a tool is invoked. - public static HookType PreToolUse { get; } = new("preToolUse"); + /// Free text. + public static McpPlanScalarValueType String { get; } = new("string"); - /// Runs before an MCP tool is invoked. - public static HookType PreMcpToolCall { get; } = new("preMcpToolCall"); + /// A number. + public static McpPlanScalarValueType Number { get; } = new("number"); - /// Runs after a tool completes successfully. - public static HookType PostToolUse { get; } = new("postToolUse"); + /// A boolean. + public static McpPlanScalarValueType Boolean { get; } = new("boolean"); - /// Runs after a tool fails. - public static HookType PostToolUseFailure { get; } = new("postToolUseFailure"); + /// A filesystem path. + public static McpPlanScalarValueType Path { get; } = new("path"); - /// Runs after the user submits a prompt. - public static HookType UserPromptSubmitted { get; } = new("userPromptSubmitted"); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpPlanScalarValueType left, McpPlanScalarValueType right) => left.Equals(right); - /// 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"); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpPlanScalarValueType left, McpPlanScalarValueType right) => !(left == right); - /// Runs when a session starts. - public static HookType SessionStart { get; } = new("sessionStart"); + /// + public override bool Equals(object? obj) => obj is McpPlanScalarValueType other && Equals(other); - /// Runs when a session ends. - public static HookType SessionEnd { get; } = new("sessionEnd"); + /// + public bool Equals(McpPlanScalarValueType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - /// Runs after an agent result is produced. - public static HookType PostResult { get; } = new("postResult"); + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - /// Runs before a pull request description is generated. - public static HookType PrePRDescription { get; } = new("prePRDescription"); + /// + public override string ToString() => Value; - /// Runs when the agent encounters an error. - public static HookType ErrorOccurred { get; } = new("errorOccurred"); + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override McpPlanScalarValueType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } - /// Runs when the agent stops. - public static HookType AgentStop { get; } = new("agentStop"); + /// + public override void Write(Utf8JsonWriter writer, McpPlanScalarValueType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanScalarValueType)); + } + } +} - /// Runs when a subagent starts. - public static HookType SubagentStart { get; } = new("subagentStart"); - /// Runs when a subagent stops. - public static HookType SubagentStop { get; } = new("subagentStop"); +/// Discriminator for an enumerated required value. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpPlanEnumValueType : IEquatable +{ + private readonly string? _value; - /// Runs before conversation context is compacted. - public static HookType PreCompact { get; } = new("preCompact"); + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpPlanEnumValueType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } - /// Runs when the agent requests permission. - public static HookType PermissionRequest { get; } = new("permissionRequest"); + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; - /// Runs when the agent emits a notification. - public static HookType Notification { get; } = new("notification"); + /// 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 ==(HookType left, HookType 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 !=(HookType left, HookType 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 HookType other && Equals(other); + public override bool Equals(object? obj) => obj is McpPlanEnumValueType other && Equals(other); /// - public bool Equals(HookType 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); @@ -25587,68 +28235,59 @@ 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 McpPlanEnumValueType 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, McpPlanEnumValueType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HookType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanEnumValueType)); } } } -/// Configuration tier that contributed a discovered hook action. +/// Transport exposed by a locally launched package. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct HookOrigin : 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 HookOrigin(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; - /// 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"); + /// 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 ==(HookOrigin left, HookOrigin 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 !=(HookOrigin left, HookOrigin 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 HookOrigin other && Equals(other); + public override bool Equals(object? obj) => obj is McpPlanPackageTransport other && Equals(other); /// - public bool Equals(HookOrigin 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); @@ -25656,68 +28295,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 McpPlanPackageTransport 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, McpPlanPackageTransport value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HookOrigin)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPackageTransport)); } } } -/// Resolved Anthropic adaptive-thinking capability for a model. +/// Transport exposed by a remote endpoint. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AdaptiveThinkingSupport : 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 AdaptiveThinkingSupport(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 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"); + /// An HTTP endpoint. + public static McpPlanRemoteTransport Http { get; } = new("http"); - /// 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"); + /// A streamable HTTP endpoint. + public static McpPlanRemoteTransport StreamableHttp { get; } = new("streamable-http"); - /// 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"); + /// 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 ==(AdaptiveThinkingSupport left, AdaptiveThinkingSupport right) => left.Equals(right); + /// 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 !=(AdaptiveThinkingSupport left, AdaptiveThinkingSupport right) => !(left == 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 AdaptiveThinkingSupport other && Equals(other); + public override bool Equals(object? obj) => obj is McpPlanRemoteTransport other && Equals(other); /// - public bool Equals(AdaptiveThinkingSupport 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); @@ -25725,65 +28361,62 @@ 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 McpPlanRemoteTransport 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, McpPlanRemoteTransport value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AdaptiveThinkingSupport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanRemoteTransport)); } } } -/// Model capability category for grouping in the model picker. +/// Why capability and protocol-version negotiation refused a caller. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ModelPickerCategory : 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 ModelPickerCategory(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; - /// 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"); + /// The caller's protocol version is below the lowest this runtime serves. + public static CatalogNegotiationRefusedReason UnsupportedProtocolVersion { get; } = new("unsupported-protocol-version"); - /// Powerful model category optimized for complex tasks. - public static ModelPickerCategory Powerful { get; } = new("powerful"); + /// 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 ==(ModelPickerCategory left, ModelPickerCategory 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 !=(ModelPickerCategory left, ModelPickerCategory 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 ModelPickerCategory other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogNegotiationRefusedReason other && Equals(other); /// - public bool Equals(ModelPickerCategory 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); @@ -25791,68 +28424,65 @@ 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 CatalogNegotiationRefusedReason 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, CatalogNegotiationRefusedReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelPickerCategory)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNegotiationRefusedReason)); } } } -/// Relative cost tier for token-based billing users. +/// Which kind of opaque handle was presented. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ModelPickerPriceCategory : 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 ModelPickerPriceCategory(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; - /// Lowest relative token cost tier. - public static ModelPickerPriceCategory Low { get; } = new("low"); - - /// Medium relative token cost tier. - public static ModelPickerPriceCategory Medium { get; } = new("medium"); + /// A search candidate handle. + public static CatalogHandleType Candidate { get; } = new("candidate"); - /// High relative token cost tier. - public static ModelPickerPriceCategory High { get; } = new("high"); + /// An install plan handle. + public static CatalogHandleType Plan { get; } = new("plan"); - /// Highest relative token cost tier. - public static ModelPickerPriceCategory VeryHigh { get; } = new("very_high"); + /// 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 ==(ModelPickerPriceCategory left, ModelPickerPriceCategory 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 !=(ModelPickerPriceCategory left, ModelPickerPriceCategory 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 ModelPickerPriceCategory other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogHandleType other && Equals(other); /// - public bool Equals(ModelPickerPriceCategory 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); @@ -25860,65 +28490,74 @@ 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 CatalogHandleType 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, CatalogHandleType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelPickerPriceCategory)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogHandleType)); } } } -/// Current policy state for this model. +/// Why a presented handle was rejected. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ModelPolicyState : 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 ModelPolicyState(string value) + public CatalogHandleRejectionReason(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 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 model is disabled by policy. - public static ModelPolicyState Disabled { get; } = new("disabled"); + /// The handle was issued by a different runtime instance or session. + public static CatalogHandleRejectionReason Foreign { get; } = new("foreign"); - /// No explicit policy is configured for the model. - public static ModelPolicyState Unconfigured { get; } = new("unconfigured"); + /// The handle was issued for another catalog operation. + public static CatalogHandleRejectionReason WrongKind { get; } = new("wrong-kind"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ModelPolicyState left, ModelPolicyState right) => left.Equals(right); + /// 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 not equivalent. - public static bool operator !=(ModelPolicyState left, ModelPolicyState right) => !(left == right); + /// 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 ModelPolicyState other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogHandleRejectionReason other && Equals(other); /// - public bool Equals(ModelPolicyState 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); @@ -25926,62 +28565,92 @@ 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 CatalogHandleRejectionReason 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, CatalogHandleRejectionReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelPolicyState)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogHandleRejectionReason)); } } } -/// 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. +/// 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 ModelProviderKind : 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 ModelProviderKind(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; - /// GitHub Copilot / CAPI models, spawned by a github-resolving account that holds a Copilot seat. - public static ModelProviderKind Copilot { get; } = new("copilot"); + /// The selected existing attached session was missing, malformed or unavailable. + public static CatalogInvalidRequestField PolicySessionId { get; } = new("policySessionId"); - /// Microsoft 365 Copilot (Loki) inference models, spawned by a resolvable Entra-derived Loki account. - public static ModelProviderKind Loki { get; } = new("loki"); + /// 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 ==(ModelProviderKind left, ModelProviderKind 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 !=(ModelProviderKind left, ModelProviderKind 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 ModelProviderKind other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogInvalidRequestField other && Equals(other); /// - public bool Equals(ModelProviderKind 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); @@ -25989,68 +28658,65 @@ 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 CatalogInvalidRequestField 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, CatalogInvalidRequestField value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogInvalidRequestField)); } } } -/// Server transport type: stdio, http, sse (deprecated), or memory. +/// 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 DiscoveredMcpServerType : 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 DiscoveredMcpServerType(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; - /// Server communicates over stdio with a local child process. - public static DiscoveredMcpServerType Stdio { get; } = new("stdio"); - - /// Server communicates over streamable HTTP. - public static DiscoveredMcpServerType Http { get; } = new("http"); + /// No credential was presented, so there is nothing to refresh and the caller must sign in. + public static CatalogAuthenticationRequiredReason NoCredential { get; } = new("no-credential"); - /// Server communicates over Server-Sent Events (deprecated). - public static DiscoveredMcpServerType Sse { get; } = new("sse"); + /// 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"); - /// Server is backed by an in-memory runtime implementation. - public static DiscoveredMcpServerType Memory { get; } = new("memory"); + /// 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 ==(DiscoveredMcpServerType left, DiscoveredMcpServerType 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 !=(DiscoveredMcpServerType left, DiscoveredMcpServerType 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 DiscoveredMcpServerType other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogAuthenticationRequiredReason other && Equals(other); /// - public bool Equals(DiscoveredMcpServerType 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); @@ -26058,98 +28724,89 @@ 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 CatalogAuthenticationRequiredReason 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, CatalogAuthenticationRequiredReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredMcpServerType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAuthenticationRequiredReason)); } } } -/// 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. +/// 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 CatalogCapability : 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 CatalogCapability(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; - /// 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"); + /// No network is available, so nothing was attempted. + public static CatalogNetworkFailureReason Offline { get; } = new("offline"); - /// 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"); + /// The authority's name could not be resolved. + public static CatalogNetworkFailureReason Dns { get; } = new("dns"); - /// Understands plans that enumerate every eligible transport rather than a single preferred one. - public static CatalogCapability MultipleTransportChoice { get; } = new("multiple-transport-choice"); + /// The request exceeded its time budget. + public static CatalogNetworkFailureReason Timeout { get; } = new("timeout"); - /// 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"); + /// The TLS handshake or certificate validation failed. + public static CatalogNetworkFailureReason Tls { get; } = new("tls"); - /// 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"); + /// The connection was refused or reset. + public static CatalogNetworkFailureReason ConnectionRefused { get; } = new("connection-refused"); - /// Understands exact candidate selection through model-safe opaque references and host-only candidate-handle hand-off. - public static CatalogCapability CatalogSelection { get; } = new("catalog-selection"); + /// The configured proxy returned 407 and requires authentication. + public static CatalogNetworkFailureReason ProxyAuthenticationRequired { get; } = new("proxy-authentication-required"); - /// 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"); + /// The authority rate-limited requests and supplied or implied a bounded cooldown. + public static CatalogNetworkFailureReason RateLimited { get; } = new("rate-limited"); - /// 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"); + /// The authority returned a transient 5xx response. + public static CatalogNetworkFailureReason ServiceUnavailable { get; } = new("service-unavailable"); - /// 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"); + /// The authority returned another status the runtime treats as a failure. + public static CatalogNetworkFailureReason HttpStatus { get; } = new("http-status"); - /// 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"); + /// The response exceeded the permitted size. + public static CatalogNetworkFailureReason ResponseTooLarge { get; } = new("response-too-large"); - /// 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"); + /// 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 ==(CatalogCapability left, CatalogCapability right) => left.Equals(right); + /// 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 !=(CatalogCapability left, CatalogCapability right) => !(left == 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 CatalogCapability other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogNetworkFailureReason other && Equals(other); /// - public bool Equals(CatalogCapability 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); @@ -26157,62 +28814,74 @@ 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 CatalogNetworkFailureReason 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, CatalogNetworkFailureReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogCapability)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNetworkFailureReason)); } } } -/// Whether a planned configuration change would create or modify an entry. +/// Which hardened-fetch control refused a retrieval. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanConfigurationOperation : 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 McpPlanConfigurationOperation(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; - /// Creates a configuration entry that does not exist yet. - public static McpPlanConfigurationOperation Add { get; } = new("add"); + /// The URL used a scheme the runtime refuses to fetch. + public static CatalogUnsafeRetrievalReason BlockedScheme { get; } = new("blocked-scheme"); - /// Modifies a configuration entry that already exists. - public static McpPlanConfigurationOperation Update { get; } = new("update"); + /// The URL embedded credentials. + public static CatalogUnsafeRetrievalReason CredentialsInUrl { get; } = new("credentials-in-url"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpPlanConfigurationOperation left, McpPlanConfigurationOperation right) => left.Equals(right); + /// The URL resolved to a loopback, private, link-local, or cloud metadata address. + public static CatalogUnsafeRetrievalReason BlockedAddress { get; } = new("blocked-address"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpPlanConfigurationOperation left, McpPlanConfigurationOperation right) => !(left == right); + /// 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 McpPlanConfigurationOperation other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogUnsafeRetrievalReason other && Equals(other); /// - public bool Equals(McpPlanConfigurationOperation 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); @@ -26220,59 +28889,68 @@ 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 CatalogUnsafeRetrievalReason 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, CatalogUnsafeRetrievalReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanConfigurationOperation)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnsafeRetrievalReason)); } } } -/// Configuration scope an MCP install plan targets. +/// Media type a catalog card is interpreted as. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanScope : 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 McpPlanScope(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; - /// The user's own MCP configuration. - public static McpPlanScope User { get; } = new("user"); + /// 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 ==(McpPlanScope left, McpPlanScope 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 !=(McpPlanScope left, McpPlanScope 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 McpPlanScope other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogMediaType other && Equals(other); /// - public bool Equals(McpPlanScope 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); @@ -26280,65 +28958,71 @@ 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 CatalogMediaType 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, CatalogMediaType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanScope)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMediaType)); } } } -/// What policy decided for a planned server. +/// How a card failed validation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanPolicyDecision : 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 McpPlanPolicyDecision(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; - /// Policy permits the server. - public static McpPlanPolicyDecision Allowed { get; } = new("allowed"); + /// The document is not well-formed JSON. + public static CatalogMalformedCardReason InvalidJson { get; } = new("invalid-json"); - /// Policy forbids the server, so the plan cannot be applied. - public static McpPlanPolicyDecision Blocked { get; } = new("blocked"); + /// The document does not satisfy its media type's schema. + public static CatalogMalformedCardReason SchemaViolation { get; } = new("schema-violation"); - /// Policy permits the server only after an explicit approval. - public static McpPlanPolicyDecision RequiresApproval { get; } = new("requires-approval"); + /// 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 ==(McpPlanPolicyDecision left, McpPlanPolicyDecision 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 !=(McpPlanPolicyDecision left, McpPlanPolicyDecision 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 McpPlanPolicyDecision other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogMalformedCardReason other && Equals(other); /// - public bool Equals(McpPlanPolicyDecision 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); @@ -26346,68 +29030,68 @@ 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 CatalogMalformedCardReason 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, CatalogMalformedCardReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPolicyDecision)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMalformedCardReason)); } } } -/// Which authority produced a policy decision. +/// Which wire-contract rule an upstream response broke. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanPolicySource : 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 McpPlanPolicySource(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; - /// No policy applied, so the server is permitted by default. - public static McpPlanPolicySource None { get; } = new("none"); + /// A result carried both a URL and embedded data, when exactly one is permitted. + public static CatalogContractViolationReason BothUrlAndData { get; } = new("both-url-and-data"); - /// An enterprise allowlist evaluated the server. - public static McpPlanPolicySource EnterpriseAllowlist { get; } = new("enterprise-allowlist"); + /// 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 registry the card came from evaluated the server. - public static McpPlanPolicySource RegistryPolicy { get; } = new("registry-policy"); + /// 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"); - /// Local trust settings evaluated the server. - public static McpPlanPolicySource LocalTrust { get; } = new("local-trust"); + /// 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 ==(McpPlanPolicySource left, McpPlanPolicySource right) => left.Equals(right); + /// 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 !=(McpPlanPolicySource left, McpPlanPolicySource right) => !(left == 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 McpPlanPolicySource other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogContractViolationReason other && Equals(other); /// - public bool Equals(McpPlanPolicySource 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); @@ -26415,59 +29099,65 @@ 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 CatalogContractViolationReason 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, CatalogContractViolationReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPolicySource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogContractViolationReason)); } } } -/// Canonical digest algorithm for a validated MCP card. +/// Why no usable transport could be offered. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CardDigestAlgorithm : 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 CardDigestAlgorithm(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; - /// SHA-256 over RFC 8785 canonical JSON encoded as UTF-8. - public static CardDigestAlgorithm Sha256Rfc8785 { get; } = new("sha256-rfc8785"); + /// The card advertises no transport this runtime can use. + public static CatalogUnavailableTransportReason NoEligibleTransport { get; } = new("no-eligible-transport"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CardDigestAlgorithm left, CardDigestAlgorithm right) => left.Equals(right); + /// Every advertised transport is of a kind this runtime does not implement. + public static CatalogUnavailableTransportReason TransportNotSupported { get; } = new("transport-not-supported"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CardDigestAlgorithm left, CardDigestAlgorithm right) => !(left == right); + /// 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 ==(CatalogUnavailableTransportReason left, CatalogUnavailableTransportReason right) => left.Equals(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 CardDigestAlgorithm other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogUnavailableTransportReason other && Equals(other); /// - public bool Equals(CardDigestAlgorithm 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); @@ -26475,62 +29165,65 @@ 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 CatalogUnavailableTransportReason 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, CatalogUnavailableTransportReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CardDigestAlgorithm)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnavailableTransportReason)); } } } -/// JSON MCP card media type accepted for install planning. +/// Why a discoverable candidate cannot be installed. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpServerCardMediaType : 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 McpServerCardMediaType(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 current MCP server card media type. - public static McpServerCardMediaType ApplicationMcpServerCardJson { get; } = new("application/mcp-server-card+json"); + /// This kind of resource is not installable through this surface. + public static CatalogNotInstallableReason KindNotInstallable { get; } = new("kind-not-installable"); - /// The legacy MCP server card media type, accepted for compatibility. - public static McpServerCardMediaType ApplicationMcpServerJson { get; } = new("application/mcp-server+json"); + /// 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 ==(McpServerCardMediaType left, McpServerCardMediaType 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 !=(McpServerCardMediaType left, McpServerCardMediaType 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 McpServerCardMediaType other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogNotInstallableReason other && Equals(other); /// - public bool Equals(McpServerCardMediaType 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); @@ -26538,71 +29231,71 @@ 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 CatalogNotInstallableReason 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, CatalogNotInstallableReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpServerCardMediaType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNotInstallableReason)); } } } -/// Where a required value is applied when the planned server is launched. +/// 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 McpPlanValueCategory : 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 McpPlanValueCategory(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; - /// Set as an environment variable on the launched process. - public static McpPlanValueCategory EnvironmentVariable { get; } = new("environment-variable"); + /// Bounded search is not wired up on this runtime build. + public static CatalogUnavailableReason SearchUnavailable { get; } = new("search-unavailable"); - /// Passed to the runtime that launches the package. - public static McpPlanValueCategory RuntimeArgument { get; } = new("runtime-argument"); + /// Install planning is not wired up on this runtime build. + public static CatalogUnavailableReason PlanningUnavailable { get; } = new("planning-unavailable"); - /// Passed to the packaged server itself. - public static McpPlanValueCategory PackageArgument { get; } = new("package-argument"); + /// Exact candidate selection is not available in this session or runtime. + public static CatalogUnavailableReason SelectionUnavailable { get; } = new("selection-unavailable"); - /// Sent as a request header to a remote endpoint. - public static McpPlanValueCategory Header { get; } = new("header"); + /// No catalog authority is configured for this runtime. + public static CatalogUnavailableReason AuthorityNotConfigured { get; } = new("authority-not-configured"); - /// Substituted into the remote endpoint URL. - public static McpPlanValueCategory UrlVariable { get; } = new("url-variable"); + /// 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 ==(McpPlanValueCategory left, McpPlanValueCategory 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 !=(McpPlanValueCategory left, McpPlanValueCategory 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 McpPlanValueCategory other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogUnavailableReason other && Equals(other); /// - public bool Equals(McpPlanValueCategory 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); @@ -26610,68 +29303,68 @@ 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 CatalogUnavailableReason 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, CatalogUnavailableReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanValueCategory)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnavailableReason)); } } } -/// Scalar type a required value must conform to. +/// 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 McpPlanScalarValueType : 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 McpPlanScalarValueType(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; - /// Free text. - public static McpPlanScalarValueType String { get; } = new("string"); + /// Eligible for default catalogue exposure. + public static CatalogTrustEligibility Default { get; } = new("default"); - /// A number. - public static McpPlanScalarValueType Number { get; } = new("number"); + /// Eligible only when expanded or community results are requested. + public static CatalogTrustEligibility Expanded { get; } = new("expanded"); - /// A boolean. - public static McpPlanScalarValueType Boolean { get; } = new("boolean"); + /// Not eligible for normal catalogue exposure. + public static CatalogTrustEligibility Hidden { get; } = new("hidden"); - /// A filesystem path. - public static McpPlanScalarValueType Path { get; } = new("path"); + /// The authority did not supply an eligibility decision. + public static CatalogTrustEligibility Unknown { get; } = new("unknown"); - /// 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 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 !=(McpPlanScalarValueType left, McpPlanScalarValueType right) => !(left == 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 McpPlanScalarValueType other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogTrustEligibility other && Equals(other); /// - public bool Equals(McpPlanScalarValueType 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); @@ -26679,59 +29372,59 @@ 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 CatalogTrustEligibility 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, CatalogTrustEligibility value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanScalarValueType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustEligibility)); } } } -/// Discriminator for an enumerated required value. +/// Bounded authority that supplied a catalogue trust observation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanEnumValueType : 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 McpPlanEnumValueType(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; - /// One of a fixed, non-empty set of permitted values. - public static McpPlanEnumValueType Enum { get; } = new("enum"); + /// 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 ==(McpPlanEnumValueType left, McpPlanEnumValueType 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 !=(McpPlanEnumValueType left, McpPlanEnumValueType 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 McpPlanEnumValueType other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogTrustSource other && Equals(other); /// - public bool Equals(McpPlanEnumValueType 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); @@ -26739,59 +29432,59 @@ 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 CatalogTrustSource 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, CatalogTrustSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanEnumValueType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustSource)); } } } -/// Transport exposed by a locally launched package. +/// Schema version of the catalogue trust snapshot envelope. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpPlanPackageTransport : 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 McpPlanPackageTransport(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; - /// A locally launched process spoken to over standard input and output. - public static McpPlanPackageTransport Stdio { get; } = new("stdio"); + /// 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 ==(McpPlanPackageTransport left, McpPlanPackageTransport 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 !=(McpPlanPackageTransport left, McpPlanPackageTransport 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 McpPlanPackageTransport other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogTrustSnapshotSchemaVersion other && Equals(other); /// - public bool Equals(McpPlanPackageTransport 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); @@ -26799,65 +29492,62 @@ 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 CatalogTrustSnapshotSchemaVersion 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, CatalogTrustSnapshotSchemaVersion value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanPackageTransport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustSnapshotSchemaVersion)); } } } -/// Transport exposed by a remote endpoint. +/// 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 McpPlanRemoteTransport : 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 McpPlanRemoteTransport(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; - /// An HTTP endpoint. - public static McpPlanRemoteTransport Http { get; } = new("http"); - - /// A streamable HTTP endpoint. - public static McpPlanRemoteTransport StreamableHttp { get; } = new("streamable-http"); + /// Tier one as assigned by the catalogue authority. + public static CatalogTrustTier T1 { get; } = new("T1"); - /// A server-sent events endpoint. - public static McpPlanRemoteTransport Sse { get; } = new("sse"); + /// 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 ==(McpPlanRemoteTransport left, McpPlanRemoteTransport 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 !=(McpPlanRemoteTransport left, McpPlanRemoteTransport 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 McpPlanRemoteTransport other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogTrustTier other && Equals(other); /// - public bool Equals(McpPlanRemoteTransport 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); @@ -26865,62 +29555,74 @@ 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 CatalogTrustTier 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, CatalogTrustTier value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPlanRemoteTransport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustTier)); } } } -/// Why capability and protocol-version negotiation refused a caller. +/// Configuration ownership and setup observations, distinct from tool permissions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogNegotiationRefusedReason : 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 CatalogNegotiationRefusedReason(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 caller's protocol version is below the lowest this runtime serves. - public static CatalogNegotiationRefusedReason UnsupportedProtocolVersion { get; } = new("unsupported-protocol-version"); + /// Owned configuration exists; inventory alone does not grant activation. + public static McpInstallationState NeedsSetup { get; } = new("needs-setup"); - /// The caller requires at least one capability this runtime cannot honour. - public static CatalogNegotiationRefusedReason UnsupportedCapability { get; } = new("unsupported-capability"); + /// The selected authorised session reports an active installation. + public static McpInstallationState Active { get; } = new("active"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogNegotiationRefusedReason left, CatalogNegotiationRefusedReason right) => left.Equals(right); + /// The selected server requires explicit sign-in. + public static McpInstallationState AuthenticationRequired { get; } = new("authentication-required"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogNegotiationRefusedReason left, CatalogNegotiationRefusedReason right) => !(left == right); + /// The selected server could not be activated. + public static McpInstallationState ActivationFailed { get; } = new("activation-failed"); + + /// Owned configuration no longer matches its receipt. + public static McpInstallationState ConfigurationModified { get; } = new("configuration-modified"); + + /// Confirmed durable work or unsafe evidence requires recovery. + public static McpInstallationState RecoveryRequired { get; } = new("recovery-required"); + + /// 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 CatalogNegotiationRefusedReason other && Equals(other); + public override bool Equals(object? obj) => obj is McpInstallationState other && Equals(other); /// - public bool Equals(CatalogNegotiationRefusedReason 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); @@ -26928,65 +29630,125 @@ 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 McpInstallationState 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, McpInstallationState value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNegotiationRefusedReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpInstallationState)); } } } -/// Which kind of opaque handle was presented. +/// Bounded refusal categories, without echoing handles, credentials or configuration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogHandleType : 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 CatalogHandleType(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 search candidate handle. - public static CatalogHandleType Candidate { get; } = new("candidate"); + /// The selected choice or request is unsupported or malformed. + public static McpInstallationFailureReason InvalidRequest { get; } = new("invalid-request"); - /// An install plan handle. - public static CatalogHandleType Plan { get; } = new("plan"); + /// The bounded original-connection operation limit was reached. + public static McpInstallationFailureReason OperationLimit { get; } = new("operation-limit"); - /// A model-safe reference to one retained search candidate. - public static CatalogHandleType Selection { get; } = new("selection"); + /// The original operation was cancelled. + public static McpInstallationFailureReason Cancelled { get; } = new("cancelled"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogHandleType left, CatalogHandleType right) => left.Equals(right); + /// Required installation capabilities were omitted. + public static McpInstallationFailureReason CapabilityRequired { get; } = new("capability-required"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogHandleType left, CatalogHandleType right) => !(left == right); + /// 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"); + + /// 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 CatalogHandleType other && Equals(other); + public override bool Equals(object? obj) => obj is McpInstallationFailureReason other && Equals(other); /// - public bool Equals(CatalogHandleType 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); @@ -26994,74 +29756,62 @@ 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 McpInstallationFailureReason 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, McpInstallationFailureReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogHandleType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpInstallationFailureReason)); } } } -/// Why a presented handle was rejected. +/// 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 CatalogHandleRejectionReason : 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 CatalogHandleRejectionReason(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 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"); + /// The selected operating-system keychain, without fallback to file storage. + public static McpInstallationSecretStorage Keychain { get; } = new("keychain"); - /// The supplied search identifier does not match the retained candidate. - public static CatalogHandleRejectionReason SearchMismatch { get; } = new("search-mismatch"); + /// The explicitly selected private file backend. + public static McpInstallationSecretStorage PrivateFile { get; } = new("private-file"); - /// 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 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 !=(CatalogHandleRejectionReason left, CatalogHandleRejectionReason right) => !(left == 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 CatalogHandleRejectionReason other && Equals(other); + public override bool Equals(object? obj) => obj is McpInstallationSecretStorage other && Equals(other); /// - public bool Equals(CatalogHandleRejectionReason 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); @@ -27069,92 +29819,62 @@ 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 McpInstallationSecretStorage 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, McpInstallationSecretStorage value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogHandleRejectionReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpInstallationSecretStorage)); } } } -/// Which request field was rejected locally or by the catalog authority. +/// Persisted extension discovery source. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogInvalidRequestField : 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 CatalogInvalidRequestField(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; - /// 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"); - - /// 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"); + /// Extension discovered from the user's extensions directory. + public static DiscoveredExtensionSource User { get; } = new("user"); - /// The terminal selection outcome was missing or unsupported. - public static CatalogInvalidRequestField Outcome { get; } = new("outcome"); + /// 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 ==(CatalogInvalidRequestField left, CatalogInvalidRequestField 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 !=(CatalogInvalidRequestField left, CatalogInvalidRequestField 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 CatalogInvalidRequestField other && Equals(other); + public override bool Equals(object? obj) => obj is DiscoveredExtensionSource other && Equals(other); /// - public bool Equals(CatalogInvalidRequestField 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); @@ -27162,65 +29882,65 @@ 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 DiscoveredExtensionSource 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, DiscoveredExtensionSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogInvalidRequestField)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredExtensionSource)); } } } -/// Why the catalog authority did not accept the caller's identity. +/// Effective extension loading and agent-management mode. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogAuthenticationRequiredReason : 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 CatalogAuthenticationRequiredReason(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; - /// No credential was presented, so there is nothing to refresh and the caller must sign in. - public static CatalogAuthenticationRequiredReason NoCredential { get; } = new("no-credential"); + /// Extensions are not loaded. + public static DiscoveredExtensionMode Disabled { get; } = new("disabled"); - /// 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"); + /// Extensions are loaded, but the agent cannot create, reload, or manage them. + public static DiscoveredExtensionMode LoadOnly { get; } = new("load_only"); - /// 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"); + /// 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 ==(CatalogAuthenticationRequiredReason left, CatalogAuthenticationRequiredReason 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 !=(CatalogAuthenticationRequiredReason left, CatalogAuthenticationRequiredReason 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 CatalogAuthenticationRequiredReason other && Equals(other); + public override bool Equals(object? obj) => obj is DiscoveredExtensionMode other && Equals(other); /// - public bool Equals(CatalogAuthenticationRequiredReason 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); @@ -27228,89 +29948,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 DiscoveredExtensionMode 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, DiscoveredExtensionMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAuthenticationRequiredReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredExtensionMode)); } } } -/// Categorised network failure, low cardinality so it can be aggregated without carrying a URL. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogNetworkFailureReason : 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 CatalogNetworkFailureReason(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; - /// 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"); + /// 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 ==(CatalogNetworkFailureReason left, CatalogNetworkFailureReason 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 !=(CatalogNetworkFailureReason left, CatalogNetworkFailureReason 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 CatalogNetworkFailureReason other && Equals(other); + public override bool Equals(object? obj) => obj is SkillInstallationScope other && Equals(other); /// - public bool Equals(CatalogNetworkFailureReason 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); @@ -27318,74 +30008,65 @@ 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 SkillInstallationScope 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, SkillInstallationScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNetworkFailureReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationScope)); } } } -/// Which hardened-fetch control refused a retrieval. +/// Owned Skill state observed from files and receipts. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogUnsafeRetrievalReason : 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 CatalogUnsafeRetrievalReason(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; - /// 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"); + /// Owned files and receipt evidence match. + public static SkillInstallationOwnershipState Intact { get; } = new("intact"); - /// The configured proxy policy refused the request. - public static CatalogUnsafeRetrievalReason ProxyRejected { get; } = new("proxy-rejected"); + /// Owned files no longer match the receipt. + public static SkillInstallationOwnershipState Modified { get; } = new("modified"); - /// The authority is not permitted for card retrieval. - public static CatalogUnsafeRetrievalReason HostNotPermitted { get; } = new("host-not-permitted"); + /// 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 ==(CatalogUnsafeRetrievalReason left, CatalogUnsafeRetrievalReason right) => left.Equals(right); + /// 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 !=(CatalogUnsafeRetrievalReason left, CatalogUnsafeRetrievalReason right) => !(left == 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 CatalogUnsafeRetrievalReason other && Equals(other); + public override bool Equals(object? obj) => obj is SkillInstallationOwnershipState other && Equals(other); /// - public bool Equals(CatalogUnsafeRetrievalReason 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); @@ -27393,68 +30074,68 @@ 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 SkillInstallationOwnershipState 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, SkillInstallationOwnershipState value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnsafeRetrievalReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationOwnershipState)); } } } -/// Media type a catalog card is interpreted as. +/// Bound-session observation after reconciling persisted enablement. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogMediaType : 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 CatalogMediaType(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; - /// The current MCP server card media type. - public static CatalogMediaType ApplicationMcpServerCardJson { get; } = new("application/mcp-server-card+json"); + /// The selected session has loaded this Skill and it is enabled. + public static SkillInstallationSessionState LoadedEnabled { get; } = new("loaded-enabled"); - /// The legacy MCP server card media type, accepted for compatibility. - public static CatalogMediaType ApplicationMcpServerJson { get; } = new("application/mcp-server+json"); + /// The selected session has loaded this Skill or settings and it is disabled. + public static SkillInstallationSessionState LoadedDisabled { get; } = new("loaded-disabled"); - /// An AI skill card. Representable and searchable, but typed non-installable. - public static CatalogMediaType ApplicationAiSkill { get; } = new("application/ai-skill"); + /// The selected session has not loaded Skills after the latest change. + public static SkillInstallationSessionState NotLoaded { get; } = new("not-loaded"); - /// An inert Agent Plugin descriptor. - public static CatalogMediaType ApplicationVndGitHubCopilotPlugin { get; } = new("application/vnd.github.copilot-plugin"); + /// 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 ==(CatalogMediaType left, CatalogMediaType right) => left.Equals(right); + /// 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 !=(CatalogMediaType left, CatalogMediaType right) => !(left == 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 CatalogMediaType other && Equals(other); + public override bool Equals(object? obj) => obj is SkillInstallationSessionState other && Equals(other); /// - public bool Equals(CatalogMediaType 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); @@ -27462,71 +30143,146 @@ 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 SkillInstallationSessionState 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, SkillInstallationSessionState value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMediaType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationSessionState)); } } } -/// How a card failed validation. +/// Bounded refusal categories for verified Skill installation management. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogMalformedCardReason : 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 CatalogMalformedCardReason(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; - /// The document is not well-formed JSON. - public static CatalogMalformedCardReason InvalidJson { get; } = new("invalid-json"); + /// 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 document does not satisfy its media type's schema. - public static CatalogMalformedCardReason SchemaViolation { get; } = new("schema-violation"); + /// The request is unsupported or malformed. + public static SkillInstallationFailureReason InvalidRequest { get; } = new("invalid-request"); - /// The declared media type is not one this runtime understands. - public static CatalogMalformedCardReason UnsupportedMediaType { get; } = new("unsupported-media-type"); + /// The bounded operation limit was reached. + public static SkillInstallationFailureReason OperationLimit { get; } = new("operation-limit"); - /// A field the media type requires is absent. - public static CatalogMalformedCardReason MissingRequiredField { get; } = new("missing-required-field"); + /// The original operation was cancelled. + public static SkillInstallationFailureReason Cancelled { get; } = new("cancelled"); - /// The document exceeded the permitted size. - public static CatalogMalformedCardReason SizeLimitExceeded { get; } = new("size-limit-exceeded"); + /// The original host cannot receive human confirmation. + public static SkillInstallationFailureReason ConfirmationUnavailable { get; } = new("confirmation-unavailable"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogMalformedCardReason left, CatalogMalformedCardReason right) => left.Equals(right); + /// The confirmation response is malformed or mismatched. + public static SkillInstallationFailureReason ConfirmationInvalid { get; } = new("confirmation-invalid"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogMalformedCardReason left, CatalogMalformedCardReason right) => !(left == right); + /// 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 CatalogMalformedCardReason other && Equals(other); + public override bool Equals(object? obj) => obj is SkillInstallationFailureReason other && Equals(other); /// - public bool Equals(CatalogMalformedCardReason 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); @@ -27534,68 +30290,68 @@ 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 SkillInstallationFailureReason 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, SkillInstallationFailureReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMalformedCardReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationFailureReason)); } } } -/// Which wire-contract rule an upstream response broke. +/// Which tier this directory belongs to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogContractViolationReason : 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 CatalogContractViolationReason(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; - /// A result carried both a URL and embedded data, when exactly one is permitted. - public static CatalogContractViolationReason BothUrlAndData { get; } = new("both-url-and-data"); + /// A project's repository skill directory. + public static SkillDiscoveryScope Project { get; } = new("project"); - /// 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 user's personal Copilot skill directory. + public static SkillDiscoveryScope PersonalCopilot { get; } = new("personal-copilot"); - /// 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 user's personal agents skill directory. + public static SkillDiscoveryScope PersonalAgents { get; } = new("personal-agents"); - /// A result declared no media type, or one this contract does not model. - public static CatalogContractViolationReason UnknownMediaType { get; } = new("unknown-media-type"); + /// 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 ==(CatalogContractViolationReason left, CatalogContractViolationReason 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 !=(CatalogContractViolationReason left, CatalogContractViolationReason 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 CatalogContractViolationReason other && Equals(other); + public override bool Equals(object? obj) => obj is SkillDiscoveryScope other && Equals(other); /// - public bool Equals(CatalogContractViolationReason 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); @@ -27603,65 +30359,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 SkillDiscoveryScope 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, SkillDiscoveryScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogContractViolationReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillDiscoveryScope)); } } } -/// Why no usable transport could be offered. +/// 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 CatalogUnavailableTransportReason : 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 CatalogUnavailableTransportReason(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 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"); + /// An install plan can be computed for this MCP server candidate. + public static CatalogMcpServerInstallability Installable { get; } = new("installable"); - /// Eligible remotes could not be enumerated, so no explicit choice can be offered. - public static CatalogUnavailableTransportReason RemoteEnumerationUnavailable { get; } = new("remote-enumeration-unavailable"); + /// 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 ==(CatalogUnavailableTransportReason left, CatalogUnavailableTransportReason 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 !=(CatalogUnavailableTransportReason left, CatalogUnavailableTransportReason 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 CatalogUnavailableTransportReason other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogMcpServerInstallability other && Equals(other); /// - public bool Equals(CatalogUnavailableTransportReason 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); @@ -27669,65 +30422,71 @@ 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 CatalogMcpServerInstallability 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, CatalogMcpServerInstallability value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnavailableTransportReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMcpServerInstallability)); } } } -/// Why a discoverable candidate cannot be installed. +/// Typed installability state for an AI skill candidate. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogNotInstallableReason : 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 CatalogNotInstallableReason(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; - /// This kind of resource is not installable through this surface. - public static CatalogNotInstallableReason KindNotInstallable { get; } = new("kind-not-installable"); + /// This AI skill candidate carries verified materialisation metadata and the selected session may plan installation. + public static CatalogAiSkillInstallability Installable { get; } = new("installable"); - /// AI skills are discoverable but have no typed importer in this phase. - public static CatalogNotInstallableReason AiSkillNotInstallable { get; } = new("ai-skill-not-installable"); + /// Skill installation is understood but disabled for the selected session. + public static CatalogAiSkillInstallability FeatureDisabled { get; } = new("feature-disabled"); - /// Policy forbids installing this candidate. - public static CatalogNotInstallableReason PolicyForbids { get; } = new("policy-forbids"); + /// 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 equivalent. - public static bool operator ==(CatalogNotInstallableReason left, CatalogNotInstallableReason right) => left.Equals(right); + /// Policy refuses Skill installation for the selected session or authority. + public static CatalogAiSkillInstallability PolicyForbids { get; } = new("policy-forbids"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogNotInstallableReason left, CatalogNotInstallableReason right) => !(left == right); + /// 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 CatalogNotInstallableReason other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogAiSkillInstallability other && Equals(other); /// - public bool Equals(CatalogNotInstallableReason 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); @@ -27735,71 +30494,59 @@ 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 CatalogAiSkillInstallability 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, CatalogAiSkillInstallability value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogNotInstallableReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAiSkillInstallability)); } } } -/// Why a catalog operation is not available on this runtime. +/// Canonical AI skill media type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogUnavailableReason : 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 CatalogUnavailableReason(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; - /// 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"); - - /// No catalog authority is configured for this runtime. - public static CatalogUnavailableReason AuthorityNotConfigured { get; } = new("authority-not-configured"); - - /// The surface is disabled by policy on this runtime. - public static CatalogUnavailableReason DisabledByPolicy { get; } = new("disabled-by-policy"); + /// 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 ==(CatalogUnavailableReason left, CatalogUnavailableReason 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 !=(CatalogUnavailableReason left, CatalogUnavailableReason 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 CatalogUnavailableReason other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogAiSkillMediaType other && Equals(other); /// - public bool Equals(CatalogUnavailableReason 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); @@ -27807,68 +30554,65 @@ 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 CatalogAiSkillMediaType 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, CatalogAiSkillMediaType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogUnavailableReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAiSkillMediaType)); } } } -/// Authority-computed exposure eligibility, kept separate from tier. The current tier-only Agent Finder response maps to `unknown`, never to a locally inferred eligibility. +/// 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 CatalogTrustEligibility : 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 CatalogTrustEligibility(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; - /// 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"); + /// The plugin contributes at least one GitHub Copilot Canvas. + public static CatalogAgentPluginCompatibilityTag Canvas { get; } = new("canvas"); - /// Not eligible for normal catalogue exposure. - public static CatalogTrustEligibility Hidden { get; } = new("hidden"); + /// The plugin depends on Canvas for its intended functionality. + public static CatalogAgentPluginCompatibilityTag CanvasOnly { get; } = new("canvas-only"); - /// The authority did not supply an eligibility decision. - public static CatalogTrustEligibility Unknown { get; } = new("unknown"); + /// 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 ==(CatalogTrustEligibility left, CatalogTrustEligibility 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 !=(CatalogTrustEligibility left, CatalogTrustEligibility 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 CatalogTrustEligibility other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogAgentPluginCompatibilityTag other && Equals(other); /// - public bool Equals(CatalogTrustEligibility 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); @@ -27876,59 +30620,59 @@ 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 CatalogAgentPluginCompatibilityTag 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, CatalogAgentPluginCompatibilityTag value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustEligibility)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAgentPluginCompatibilityTag)); } } } -/// Bounded authority that supplied a catalogue trust observation. +/// Canonical Agent Plugin media type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogTrustSource : 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 CatalogTrustSource(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; - /// GitHub Agent Finder supplied the trust field on its search result. - public static CatalogTrustSource AgentFinder { get; } = new("agent-finder"); + /// 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 ==(CatalogTrustSource left, CatalogTrustSource 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 !=(CatalogTrustSource left, CatalogTrustSource 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 CatalogTrustSource other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogAgentPluginMediaType other && Equals(other); /// - public bool Equals(CatalogTrustSource 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); @@ -27936,59 +30680,59 @@ 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 CatalogAgentPluginMediaType 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, CatalogAgentPluginMediaType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAgentPluginMediaType)); } } } -/// Schema version of the catalogue trust snapshot envelope. +/// 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 CatalogTrustSnapshotSchemaVersion : 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 CatalogTrustSnapshotSchemaVersion(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; - /// Initial envelope carrying one bounded service tier or one explicit unavailable state. - public static CatalogTrustSnapshotSchemaVersion V1 { get; } = new("v1"); + /// 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 ==(CatalogTrustSnapshotSchemaVersion left, CatalogTrustSnapshotSchemaVersion 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 !=(CatalogTrustSnapshotSchemaVersion left, CatalogTrustSnapshotSchemaVersion 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 CatalogTrustSnapshotSchemaVersion other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogSearchTotalCountRelation other && Equals(other); /// - public bool Equals(CatalogTrustSnapshotSchemaVersion 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); @@ -27996,62 +30740,65 @@ 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 CatalogSearchTotalCountRelation 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, CatalogSearchTotalCountRelation value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustSnapshotSchemaVersion)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogSearchTotalCountRelation)); } } } -/// Service-computed trust tier currently emitted by Agent Finder. It is independent of search score, popularity, and client-side ranking. +/// What kind of resource a catalog candidate describes. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogTrustTier : 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 CatalogTrustTier(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; - /// Tier one as assigned by the catalogue authority. - public static CatalogTrustTier T1 { get; } = new("T1"); + /// An MCP server, which can be planned for installation. + public static CatalogCandidateKind McpServer { get; } = new("mcp-server"); - /// Tier two as assigned by the catalogue authority. - public static CatalogTrustTier T2 { get; } = new("T2"); + /// An AI skill, which is discoverable but not installable through this surface. + public static CatalogCandidateKind AiSkill { get; } = new("ai-skill"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogTrustTier left, CatalogTrustTier right) => left.Equals(right); + /// 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 not equivalent. - public static bool operator !=(CatalogTrustTier left, CatalogTrustTier right) => !(left == 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 !=(CatalogCandidateKind left, CatalogCandidateKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogTrustTier other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogCandidateKind other && Equals(other); /// - public bool Equals(CatalogTrustTier 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); @@ -28059,74 +30806,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 CatalogCandidateKind 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, CatalogCandidateKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogTrustTier)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogCandidateKind)); } } } -/// Configuration ownership and setup observations, distinct from tool permissions. +/// Terminal outcome declared for a retained catalog selection group. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpInstallationState : 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 McpInstallationState(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; - /// 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"); + /// Choose the candidate named by selectionRef. + public static CatalogSelectionDecision Selected { get; } = new("selected"); - /// The selected server could not be activated. - public static McpInstallationState ActivationFailed { get; } = new("activation-failed"); + /// Explicitly decline every candidate in the search. + public static CatalogSelectionDecision Declined { get; } = new("declined"); - /// Owned configuration no longer matches its receipt. - public static McpInstallationState ConfigurationModified { get; } = new("configuration-modified"); + /// Cancel the selection interaction without choosing a candidate. + public static CatalogSelectionDecision Cancelled { get; } = new("cancelled"); - /// Confirmed durable work or unsafe evidence requires recovery. - public static McpInstallationState RecoveryRequired { get; } = new("recovery-required"); + /// Declare that the host's live interaction deadline elapsed while the reference remained valid. + public static CatalogSelectionDecision TimedOut { get; } = new("timed-out"); - /// 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 ==(CatalogSelectionDecision left, CatalogSelectionDecision 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 !=(CatalogSelectionDecision left, CatalogSelectionDecision right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpInstallationState other && Equals(other); + public override bool Equals(object? obj) => obj is CatalogSelectionDecision other && Equals(other); /// - public bool Equals(McpInstallationState other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(CatalogSelectionDecision other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -28134,125 +30875,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 CatalogSelectionDecision 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, CatalogSelectionDecision value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpInstallationState)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogSelectionDecision)); } } } -/// Bounded refusal categories, without echoing handles, credentials or configuration. +/// Where completed plugin content was staged before atomic promotion. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpInstallationFailureReason : IEquatable +public readonly struct PluginInstallStagingMode : 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 PluginInstallStagingMode(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"); + /// A sibling of the installed-plugins root, outside the recursively watched tree. + public static PluginInstallStagingMode External { get; } = new("external"); - /// A storage operation failed; inspect any allocated operation before retrying. - public static McpInstallationFailureReason 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 ==(McpInstallationFailureReason left, McpInstallationFailureReason 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 !=(McpInstallationFailureReason left, McpInstallationFailureReason 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 McpInstallationFailureReason other && Equals(other); + public override bool Equals(object? obj) => obj is PluginInstallStagingMode other && Equals(other); /// - public bool Equals(McpInstallationFailureReason 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); @@ -28260,62 +30938,74 @@ 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 PluginInstallStagingMode 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, PluginInstallStagingMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpInstallationFailureReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PluginInstallStagingMode)); } } } -/// Explicit backend selection is part of the final review; failures never switch backends. +/// Where the agent definition was loaded from. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpInstallationSecretStorage : 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 McpInstallationSecretStorage(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; - /// The selected operating-system keychain, without fallback to file storage. - public static McpInstallationSecretStorage Keychain { get; } = new("keychain"); + /// Agent loaded from the user's personal agent configuration. + public static AgentInfoSource User { get; } = new("user"); - /// The explicitly selected private file backend. - public static McpInstallationSecretStorage PrivateFile { get; } = new("private-file"); + /// Agent loaded from the current project's repository configuration. + public static AgentInfoSource Project { get; } = new("project"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpInstallationSecretStorage left, McpInstallationSecretStorage right) => left.Equals(right); + /// Agent inherited from a parent project or workspace. + public static AgentInfoSource Inherited { get; } = new("inherited"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpInstallationSecretStorage left, McpInstallationSecretStorage right) => !(left == right); + /// Agent provided by a remote runtime or service. + public static AgentInfoSource Remote { get; } = new("remote"); + + /// Agent contributed by an installed plugin. + public static AgentInfoSource Plugin { get; } = new("plugin"); + + /// 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 McpInstallationSecretStorage other && Equals(other); + public override bool Equals(object? obj) => obj is AgentInfoSource other && Equals(other); /// - public bool Equals(McpInstallationSecretStorage 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); @@ -28323,62 +31013,62 @@ 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 AgentInfoSource 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, AgentInfoSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpInstallationSecretStorage)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentInfoSource)); } } } -/// Persisted extension discovery source. +/// Which tier this directory belongs to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiscoveredExtensionSource : 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 DiscoveredExtensionSource(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; - /// Extension discovered from the user's extensions directory. - public static DiscoveredExtensionSource User { get; } = new("user"); + /// The user's personal agent configuration directory. + public static AgentDiscoveryPathScope User { get; } = new("user"); - /// Extension contributed by an installed plugin. - public static DiscoveredExtensionSource Plugin { get; } = new("plugin"); + /// 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 ==(DiscoveredExtensionSource left, DiscoveredExtensionSource 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 !=(DiscoveredExtensionSource left, DiscoveredExtensionSource 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 DiscoveredExtensionSource other && Equals(other); + public override bool Equals(object? obj) => obj is AgentDiscoveryPathScope other && Equals(other); /// - public bool Equals(DiscoveredExtensionSource 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); @@ -28386,65 +31076,68 @@ 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 AgentDiscoveryPathScope 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, AgentDiscoveryPathScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredExtensionSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentDiscoveryPathScope)); } } } -/// Effective extension loading and agent-management mode. +/// Where this source lives — used for UI grouping. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiscoveredExtensionMode : 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 DiscoveredExtensionMode(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; - /// Extensions are not loaded. - public static DiscoveredExtensionMode Disabled { get; } = new("disabled"); + /// Instructions live in user-level configuration. + public static InstructionSourceLocation User { get; } = new("user"); - /// Extensions are loaded, but the agent cannot create, reload, or manage them. - public static DiscoveredExtensionMode LoadOnly { get; } = new("load_only"); + /// Instructions live in repository-level configuration. + public static InstructionSourceLocation Repository { get; } = new("repository"); - /// Extensions are loaded and the agent can create, reload, and manage them. - public static DiscoveredExtensionMode LoadAndAugment { get; } = new("load_and_augment"); + /// Instructions live under the current working directory. + public static InstructionSourceLocation WorkingDirectory { get; } = new("working-directory"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiscoveredExtensionMode left, DiscoveredExtensionMode right) => left.Equals(right); + /// Instructions live in plugin-provided configuration. + public static InstructionSourceLocation Plugin { get; } = new("plugin"); - /// 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 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 !=(InstructionSourceLocation left, InstructionSourceLocation right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiscoveredExtensionMode other && Equals(other); + public override bool Equals(object? obj) => obj is InstructionSourceLocation other && Equals(other); /// - public bool Equals(DiscoveredExtensionMode 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); @@ -28452,59 +31145,77 @@ 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 InstructionSourceLocation 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, InstructionSourceLocation value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiscoveredExtensionMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionSourceLocation)); } } } -/// Defines the allowed values. +/// Category of instruction source — used for merge logic. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SkillInstallationScope : 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 SkillInstallationScope(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; - /// The user's personal Copilot home. - public static SkillInstallationScope Personal { get; } = new("personal"); + /// 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 ==(SkillInstallationScope left, SkillInstallationScope 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 !=(SkillInstallationScope left, SkillInstallationScope 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 SkillInstallationScope other && Equals(other); + public override bool Equals(object? obj) => obj is InstructionSourceType other && Equals(other); /// - public bool Equals(SkillInstallationScope 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); @@ -28512,65 +31223,62 @@ 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 InstructionSourceType 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, InstructionSourceType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationScope)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionSourceType)); } } } -/// Owned Skill state observed from files and receipts. +/// 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 SkillInstallationOwnershipState : 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 SkillInstallationOwnershipState(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; - /// 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"); + /// The target is a single instruction file. + public static InstructionDiscoveryPathKind File { get; } = new("file"); - /// Ownership evidence requires recovery before mutation. - public static SkillInstallationOwnershipState RecoveryRequired { get; } = new("recovery-required"); + /// 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 ==(SkillInstallationOwnershipState left, SkillInstallationOwnershipState 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 !=(SkillInstallationOwnershipState left, SkillInstallationOwnershipState 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 SkillInstallationOwnershipState other && Equals(other); + public override bool Equals(object? obj) => obj is InstructionDiscoveryPathKind other && Equals(other); /// - public bool Equals(SkillInstallationOwnershipState 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); @@ -28578,68 +31286,68 @@ 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 InstructionDiscoveryPathKind 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, InstructionDiscoveryPathKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationOwnershipState)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionDiscoveryPathKind)); } } } -/// Bound-session observation after reconciling persisted enablement. +/// Which tier this target belongs to. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SkillInstallationSessionState : 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 SkillInstallationSessionState(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; - /// The selected session has loaded this Skill and it is enabled. - public static SkillInstallationSessionState LoadedEnabled { get; } = new("loaded-enabled"); + /// Instructions live in user-level configuration. + public static InstructionDiscoveryPathLocation User { get; } = new("user"); - /// The selected session has loaded this Skill or settings and it is disabled. - public static SkillInstallationSessionState LoadedDisabled { get; } = new("loaded-disabled"); + /// Instructions live in repository-level configuration. + public static InstructionDiscoveryPathLocation Repository { get; } = new("repository"); - /// The selected session has not loaded Skills after the latest change. - public static SkillInstallationSessionState NotLoaded { get; } = new("not-loaded"); + /// Instructions live under the current working directory. + public static InstructionDiscoveryPathLocation WorkingDirectory { get; } = new("working-directory"); - /// The selected session could not be inspected. - public static SkillInstallationSessionState Unknown { get; } = new("unknown"); + /// 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 ==(SkillInstallationSessionState left, SkillInstallationSessionState right) => left.Equals(right); + /// 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 !=(SkillInstallationSessionState left, SkillInstallationSessionState right) => !(left == 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 SkillInstallationSessionState other && Equals(other); + public override bool Equals(object? obj) => obj is InstructionDiscoveryPathLocation other && Equals(other); /// - public bool Equals(SkillInstallationSessionState 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); @@ -28647,146 +31355,59 @@ 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 InstructionDiscoveryPathLocation 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, InstructionDiscoveryPathLocation value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationSessionState)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionDiscoveryPathLocation)); } } } -/// Bounded refusal categories for verified Skill installation management. +/// 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 SkillInstallationFailureReason : 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 SkillInstallationFailureReason(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; - /// 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"); - - /// 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"); + /// 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 ==(SkillInstallationFailureReason left, SkillInstallationFailureReason 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 !=(SkillInstallationFailureReason left, SkillInstallationFailureReason 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 SkillInstallationFailureReason other && Equals(other); + public override bool Equals(object? obj) => obj is SlashCommandInputCompletion other && Equals(other); /// - public bool Equals(SkillInstallationFailureReason 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); @@ -28794,68 +31415,65 @@ 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 SlashCommandInputCompletion 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, SlashCommandInputCompletion value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillInstallationFailureReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SlashCommandInputCompletion)); } } } -/// Which tier this directory belongs to. +/// 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 SkillDiscoveryScope : 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 SkillDiscoveryScope(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; - /// A project's repository skill directory. - public static SkillDiscoveryScope Project { get; } = new("project"); - - /// The user's personal Copilot skill directory. - public static SkillDiscoveryScope PersonalCopilot { get; } = new("personal-copilot"); + /// Command implemented by the runtime. + public static SlashCommandKind Builtin { get; } = new("builtin"); - /// The user's personal agents skill directory. - public static SkillDiscoveryScope PersonalAgents { get; } = new("personal-agents"); + /// Command backed by a skill. + public static SlashCommandKind Skill { get; } = new("skill"); - /// A configured custom skill directory. - public static SkillDiscoveryScope Custom { get; } = new("custom"); + /// 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 ==(SkillDiscoveryScope left, SkillDiscoveryScope 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 !=(SkillDiscoveryScope left, SkillDiscoveryScope 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 SkillDiscoveryScope other && Equals(other); + public override bool Equals(object? obj) => obj is SlashCommandKind other && Equals(other); /// - public bool Equals(SkillDiscoveryScope 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); @@ -28863,62 +31481,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 SlashCommandKind 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, SlashCommandKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SkillDiscoveryScope)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SlashCommandKind)); } } } -/// Whether an MCP server candidate can be planned for installation. +/// Severity of a managed-settings validation finding. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogMcpServerInstallability : 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 CatalogMcpServerInstallability(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; - /// An install plan can be computed for this MCP server candidate. - public static CatalogMcpServerInstallability Installable { get; } = new("installable"); + /// The runtime rejects the document. + public static ManagedSettingsDiagnosticSeverity Error { get; } = new("error"); - /// Policy forbids installing this MCP server candidate. - public static CatalogMcpServerInstallability NotInstallablePolicy { get; } = new("not-installable-policy"); + /// 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 ==(CatalogMcpServerInstallability left, CatalogMcpServerInstallability 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 !=(CatalogMcpServerInstallability left, CatalogMcpServerInstallability 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 CatalogMcpServerInstallability other && Equals(other); + public override bool Equals(object? obj) => obj is ManagedSettingsDiagnosticSeverity other && Equals(other); /// - public bool Equals(CatalogMcpServerInstallability 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); @@ -28926,71 +31544,65 @@ 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 ManagedSettingsDiagnosticSeverity 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, ManagedSettingsDiagnosticSeverity value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogMcpServerInstallability)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ManagedSettingsDiagnosticSeverity)); } } } -/// Typed installability state for an AI skill candidate. +/// A channel accepted by managedSettings.compose. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogAiSkillInstallability : 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 CatalogAiSkillInstallability(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; - /// 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"); - - /// The candidate lacks verified materialisation metadata required for installation. - public static CatalogAiSkillInstallability MaterialisationUnavailable { get; } = new("materialisation-unavailable"); + /// Device policy, the strongest channel. + public static ManagedSettingsChannel Device { get; } = new("device"); - /// Policy refuses Skill installation for the selected session or authority. - public static CatalogAiSkillInstallability PolicyForbids { get; } = new("policy-forbids"); + /// Account or organization policy. + public static ManagedSettingsChannel Server { get; } = new("server"); - /// Compatibility value for discovery-only callers that did not negotiate Skill installation. - public static CatalogAiSkillInstallability NotInstallableKind { get; } = new("not-installable-kind"); + /// 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 ==(CatalogAiSkillInstallability left, CatalogAiSkillInstallability 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 !=(CatalogAiSkillInstallability left, CatalogAiSkillInstallability 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 CatalogAiSkillInstallability other && Equals(other); + public override bool Equals(object? obj) => obj is ManagedSettingsChannel other && Equals(other); /// - public bool Equals(CatalogAiSkillInstallability 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); @@ -28998,59 +31610,62 @@ 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 ManagedSettingsChannel 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, ManagedSettingsChannel value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAiSkillInstallability)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ManagedSettingsChannel)); } } } -/// Canonical AI skill media type. +/// Path conventions used by this filesystem. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogAiSkillMediaType : 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 CatalogAiSkillMediaType(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; - /// An AI skill card. - public static CatalogAiSkillMediaType ApplicationAiSkill { get; } = new("application/ai-skill"); + /// Paths use Windows path conventions. + public static SessionFsSetProviderConventions Windows { get; } = new("windows"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogAiSkillMediaType left, CatalogAiSkillMediaType right) => left.Equals(right); + /// Paths use POSIX path conventions. + public static SessionFsSetProviderConventions Posix { get; } = new("posix"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogAiSkillMediaType left, CatalogAiSkillMediaType right) => !(left == 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 !=(SessionFsSetProviderConventions left, SessionFsSetProviderConventions right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogAiSkillMediaType other && Equals(other); + public override bool Equals(object? obj) => obj is SessionFsSetProviderConventions other && Equals(other); /// - public bool Equals(CatalogAiSkillMediaType 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); @@ -29058,65 +31673,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 SessionFsSetProviderConventions 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, SessionFsSetProviderConventions value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAiSkillMediaType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionFsSetProviderConventions)); } } } -/// Explicit Agent Plugin compatibility declared by exact catalog tags. Clients must not infer these values from display text or other metadata. +/// Repository host type. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogAgentPluginCompatibilityTag : 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 CatalogAgentPluginCompatibilityTag(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; - /// 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"); + /// Session repository is hosted on GitHub. + public static SessionContextHostType GitHub { get; } = new("github"); - /// The plugin targets GitHub Copilot. - public static CatalogAgentPluginCompatibilityTag GitHubCopilot { get; } = new("github-copilot"); + /// 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 ==(CatalogAgentPluginCompatibilityTag left, CatalogAgentPluginCompatibilityTag 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 !=(CatalogAgentPluginCompatibilityTag left, CatalogAgentPluginCompatibilityTag 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 CatalogAgentPluginCompatibilityTag other && Equals(other); + public override bool Equals(object? obj) => obj is SessionContextHostType other && Equals(other); /// - public bool Equals(CatalogAgentPluginCompatibilityTag 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); @@ -29124,59 +31736,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 SessionContextHostType 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, SessionContextHostType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAgentPluginCompatibilityTag)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionContextHostType)); } } } -/// Canonical Agent Plugin media type. +/// 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 CatalogAgentPluginMediaType : 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 CatalogAgentPluginMediaType(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; - /// A GitHub Copilot Agent Plugin descriptor. - public static CatalogAgentPluginMediaType ApplicationVndGitHubCopilotPlugin { get; } = new("application/vnd.github.copilot-plugin"); + /// No turn is running. + public static RemoteSessionHostStatus Idle { get; } = new("idle"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogAgentPluginMediaType left, CatalogAgentPluginMediaType right) => left.Equals(right); + /// A turn is running. + public static RemoteSessionHostStatus Working { get; } = new("working"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogAgentPluginMediaType left, CatalogAgentPluginMediaType right) => !(left == right); + /// The session is blocked on the user: a permission prompt or an `ask_user` question. + public static RemoteSessionHostStatus InputNeeded { get; } = new("input-needed"); + + /// 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 ==(RemoteSessionHostStatus left, RemoteSessionHostStatus right) => left.Equals(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 CatalogAgentPluginMediaType other && Equals(other); + public override bool Equals(object? obj) => obj is RemoteSessionHostStatus other && Equals(other); /// - public bool Equals(CatalogAgentPluginMediaType 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); @@ -29184,59 +31805,62 @@ 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 RemoteSessionHostStatus 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, RemoteSessionHostStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogAgentPluginMediaType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(RemoteSessionHostStatus)); } } } -/// Relationship of the backend-reported count to the complete query result set. +/// 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 CatalogSearchTotalCountRelation : 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 CatalogSearchTotalCountRelation(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; - /// No exact/full-query or lower-bound guarantee is available. - public static CatalogSearchTotalCountRelation Unknown { get; } = new("unknown"); + /// GitHub Copilot coding agent task. + public static RemoteSessionMetadataTaskType Cca { get; } = new("cca"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogSearchTotalCountRelation left, CatalogSearchTotalCountRelation right) => left.Equals(right); + /// CLI remote task. + public static RemoteSessionMetadataTaskType Cli { get; } = new("cli"); - /// 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 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 !=(RemoteSessionMetadataTaskType left, RemoteSessionMetadataTaskType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is CatalogSearchTotalCountRelation other && Equals(other); + public override bool Equals(object? obj) => obj is RemoteSessionMetadataTaskType other && Equals(other); /// - public bool Equals(CatalogSearchTotalCountRelation 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); @@ -29244,65 +31868,62 @@ 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 RemoteSessionMetadataTaskType 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, RemoteSessionMetadataTaskType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogSearchTotalCountRelation)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(RemoteSessionMetadataTaskType)); } } } -/// What kind of resource a catalog candidate describes. +/// Step status. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogCandidateKind : 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 CatalogCandidateKind(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; - /// An MCP server, which can be planned for installation. - public static CatalogCandidateKind McpServer { get; } = new("mcp-server"); - - /// An AI skill, which is discoverable but not installable through this surface. - public static CatalogCandidateKind AiSkill { get; } = new("ai-skill"); + /// The step has started and has not yet finished. + public static SessionsOpenProgressStatus InProgress { get; } = new("in-progress"); - /// An inert Agent Plugin candidate, available only when explicitly requested. - public static CatalogCandidateKind 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 ==(CatalogCandidateKind left, CatalogCandidateKind 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 !=(CatalogCandidateKind left, CatalogCandidateKind 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 CatalogCandidateKind other && Equals(other); + public override bool Equals(object? obj) => obj is SessionsOpenProgressStatus other && Equals(other); /// - public bool Equals(CatalogCandidateKind 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); @@ -29310,68 +31931,74 @@ 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 SessionsOpenProgressStatus 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, SessionsOpenProgressStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogCandidateKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionsOpenProgressStatus)); } } } -/// Terminal outcome declared for a retained catalog selection group. +/// Handoff step. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct CatalogSelectionDecision : 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 CatalogSelectionDecision(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; - /// Choose the candidate named by selectionRef. - public static CatalogSelectionDecision Selected { get; } = new("selected"); + /// Loading the source session's events from the remote service. + public static SessionsOpenProgressStep LoadSession { get; } = new("load-session"); - /// Explicitly decline every candidate in the search. - public static CatalogSelectionDecision Declined { get; } = new("declined"); + /// Validating that the local repository matches the remote session's repository. + public static SessionsOpenProgressStep ValidateRepo { get; } = new("validate-repo"); - /// Cancel the selection interaction without choosing a candidate. - public static CatalogSelectionDecision Cancelled { get; } = new("cancelled"); + /// Checking the local working tree for uncommitted changes that would block the handoff. + public static SessionsOpenProgressStep CheckChanges { get; } = new("check-changes"); - /// Declare that the host's live interaction deadline elapsed while the reference remained valid. - public static CatalogSelectionDecision TimedOut { get; } = new("timed-out"); + /// Checking out the branch associated with the remote session in the local working tree. + public static SessionsOpenProgressStep CheckoutBranch { get; } = new("checkout-branch"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(CatalogSelectionDecision left, CatalogSelectionDecision right) => left.Equals(right); + /// Creating the new local session and seeding it with the source session's events. + public static SessionsOpenProgressStep CreateSession { get; } = new("create-session"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(CatalogSelectionDecision left, CatalogSelectionDecision right) => !(left == right); + /// 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 CatalogSelectionDecision other && Equals(other); + public override bool Equals(object? obj) => obj is SessionsOpenProgressStep other && Equals(other); /// - public bool Equals(CatalogSelectionDecision 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); @@ -29379,62 +32006,71 @@ 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 SessionsOpenProgressStep 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, SessionsOpenProgressStep value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CatalogSelectionDecision)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionsOpenProgressStep)); } } } -/// Where completed plugin content was staged before atomic promotion. +/// Outcome of the open request. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct PluginInstallStagingMode : 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 PluginInstallStagingMode(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; - /// A sibling of the installed-plugins root, outside the recursively watched tree. - public static PluginInstallStagingMode External { get; } = new("external"); + /// A new session was created. + public static SessionsOpenStatus Created { get; } = new("created"); - /// A sibling of the destination plugin directory, used when external staging is unavailable. - public static PluginInstallStagingMode DestinationSibling { get; } = new("destination_sibling"); + /// An existing session was loaded or reattached. + public static SessionsOpenStatus Resumed { get; } = new("resumed"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(PluginInstallStagingMode left, PluginInstallStagingMode right) => left.Equals(right); + /// No matching persisted session was found. + public static SessionsOpenStatus NotFound { get; } = new("not_found"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(PluginInstallStagingMode left, PluginInstallStagingMode right) => !(left == right); + /// Connected to an existing remote session. + public static SessionsOpenStatus Connected { get; } = new("connected"); + + /// 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 PluginInstallStagingMode other && Equals(other); + public override bool Equals(object? obj) => obj is SessionsOpenStatus other && Equals(other); /// - public bool Equals(PluginInstallStagingMode 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); @@ -29442,74 +32078,62 @@ 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 SessionsOpenStatus 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, SessionsOpenStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PluginInstallStagingMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionsOpenStatus)); } } } -/// Where the agent definition was loaded from. +/// Neutral SDK discriminator for the connected remote session kind. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentInfoSource : 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 AgentInfoSource(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; - /// 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"); - - /// Agent contributed by an installed plugin. - public static AgentInfoSource Plugin { get; } = new("plugin"); + /// Remote CLI session. + public static ConnectedRemoteSessionMetadataKind RemoteSession { get; } = new("remote-session"); - /// Agent built into the Copilot runtime. - public static AgentInfoSource Builtin { get; } = new("builtin"); + /// 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 ==(AgentInfoSource left, AgentInfoSource 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 !=(AgentInfoSource left, AgentInfoSource 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 AgentInfoSource other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectedRemoteSessionMetadataKind other && Equals(other); /// - public bool Equals(AgentInfoSource 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); @@ -29517,62 +32141,65 @@ 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 ConnectedRemoteSessionMetadataKind 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, ConnectedRemoteSessionMetadataKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentInfoSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectedRemoteSessionMetadataKind)); } } } -/// Which tier this directory belongs to. +/// 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 AgentDiscoveryPathScope : 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 AgentDiscoveryPathScope(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; - /// The user's personal agent configuration directory. - public static AgentDiscoveryPathScope User { get; } = new("user"); + /// Return only local sessions. + public static SessionSource Local { get; } = new("local"); - /// A project's repository agent directory. - public static AgentDiscoveryPathScope Project { get; } = new("project"); + /// Return only remote sessions. + public static SessionSource Remote { get; } = new("remote"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AgentDiscoveryPathScope left, AgentDiscoveryPathScope right) => left.Equals(right); + /// Return both local and remote sessions. + public static SessionSource All { get; } = new("all"); - /// 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 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 !=(SessionSource left, SessionSource right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentDiscoveryPathScope other && Equals(other); + public override bool Equals(object? obj) => obj is SessionSource other && Equals(other); /// - public bool Equals(AgentDiscoveryPathScope 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); @@ -29580,68 +32207,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 SessionSource 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, SessionSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentDiscoveryPathScope)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionSource)); } } } -/// Where this source lives — used for UI grouping. +/// 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 InstructionSourceLocation : 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 InstructionSourceLocation(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; - /// 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"); + /// The read succeeded against the requested history. + public static EventsCursorStatus Ok { get; } = new("ok"); - /// Instructions live in plugin-provided configuration. - public static InstructionSourceLocation Plugin { get; } = new("plugin"); + /// 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 ==(InstructionSourceLocation left, InstructionSourceLocation 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 !=(InstructionSourceLocation left, InstructionSourceLocation 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 InstructionSourceLocation other && Equals(other); + public override bool Equals(object? obj) => obj is EventsCursorStatus other && Equals(other); /// - public bool Equals(InstructionSourceLocation 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); @@ -29649,77 +32270,62 @@ 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 EventsCursorStatus 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, EventsCursorStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionSourceLocation)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsCursorStatus)); } } } -/// Category of instruction source — used for merge logic. +/// 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 InstructionSourceType : 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 InstructionSourceType(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; - /// 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"); - - /// 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"); + /// Page from the cursor toward newer events (default). + public static EventsReadDirection Forward { get; } = new("forward"); - /// Instructions supplied by an installed plugin. - public static InstructionSourceType Plugin { get; } = new("plugin"); + /// 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 ==(InstructionSourceType left, InstructionSourceType 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 !=(InstructionSourceType left, InstructionSourceType 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 InstructionSourceType other && Equals(other); + public override bool Equals(object? obj) => obj is EventsReadDirection other && Equals(other); /// - public bool Equals(InstructionSourceType 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); @@ -29727,62 +32333,71 @@ 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 EventsReadDirection 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, EventsReadDirection value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionSourceType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsReadDirection)); } } } -/// Whether the target is a single file or a directory of instruction files. +/// 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 InstructionDiscoveryPathKind : 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 InstructionDiscoveryPathKind(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; - /// The target is a single instruction file. - public static InstructionDiscoveryPathKind File { get; } = new("file"); + /// Session is blocked on an unrecoverable error. + public static AgentRegistryLiveTargetEntryAttentionKind Error { get; } = new("error"); - /// The target is a directory that holds instruction files. - public static InstructionDiscoveryPathKind Directory { get; } = new("directory"); + /// Session is waiting for a tool-permission decision. + public static AgentRegistryLiveTargetEntryAttentionKind Permission { get; } = new("permission"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(InstructionDiscoveryPathKind left, InstructionDiscoveryPathKind right) => left.Equals(right); + /// Session is waiting for the user to approve or reject a plan. + public static AgentRegistryLiveTargetEntryAttentionKind ExitPlan { get; } = new("exit_plan"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(InstructionDiscoveryPathKind left, InstructionDiscoveryPathKind right) => !(left == right); + /// Session is waiting on an elicitation prompt. + public static AgentRegistryLiveTargetEntryAttentionKind Elicitation { get; } = new("elicitation"); + + /// 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 !=(AgentRegistryLiveTargetEntryAttentionKind left, AgentRegistryLiveTargetEntryAttentionKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is InstructionDiscoveryPathKind other && Equals(other); + public override bool Equals(object? obj) => obj is AgentRegistryLiveTargetEntryAttentionKind other && Equals(other); /// - public bool Equals(InstructionDiscoveryPathKind 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); @@ -29790,68 +32405,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 AgentRegistryLiveTargetEntryAttentionKind 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, AgentRegistryLiveTargetEntryAttentionKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionDiscoveryPathKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryAttentionKind)); } } } -/// Which tier this target belongs to. +/// Process kind tag for the registry entry. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct InstructionDiscoveryPathLocation : 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 InstructionDiscoveryPathLocation(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; - /// 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"); + /// Interactive Copilot CLI exposing a UI server (legacy/normal CLI process). + public static AgentRegistryLiveTargetEntryKind UiServer { get; } = new("ui-server"); - /// Instructions live in plugin-provided configuration. - public static InstructionDiscoveryPathLocation Plugin { get; } = new("plugin"); + /// 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 ==(InstructionDiscoveryPathLocation left, InstructionDiscoveryPathLocation 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 !=(InstructionDiscoveryPathLocation left, InstructionDiscoveryPathLocation 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 InstructionDiscoveryPathLocation other && Equals(other); + public override bool Equals(object? obj) => obj is AgentRegistryLiveTargetEntryKind other && Equals(other); /// - public bool Equals(InstructionDiscoveryPathLocation 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); @@ -29859,59 +32468,62 @@ 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 AgentRegistryLiveTargetEntryKind 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, AgentRegistryLiveTargetEntryKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(InstructionDiscoveryPathLocation)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryKind)); } } } -/// Optional completion hint for the input (e.g. 'directory' for filesystem path completion). +/// 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 SlashCommandInputCompletion : 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 SlashCommandInputCompletion(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; - /// Input should complete filesystem directories. - public static SlashCommandInputCompletion Directory { get; } = new("directory"); + /// Last turn ended cleanly (model returned a final assistant message). + public static AgentRegistryLiveTargetEntryLastTerminalEvent TurnEnd { get; } = new("turn_end"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SlashCommandInputCompletion left, SlashCommandInputCompletion right) => left.Equals(right); + /// Last turn was aborted (e.g. user interrupted). + public static AgentRegistryLiveTargetEntryLastTerminalEvent Abort { get; } = new("abort"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SlashCommandInputCompletion left, SlashCommandInputCompletion right) => !(left == 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 !=(AgentRegistryLiveTargetEntryLastTerminalEvent left, AgentRegistryLiveTargetEntryLastTerminalEvent right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SlashCommandInputCompletion other && Equals(other); + public override bool Equals(object? obj) => obj is AgentRegistryLiveTargetEntryLastTerminalEvent other && Equals(other); /// - public bool Equals(SlashCommandInputCompletion 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); @@ -29919,65 +32531,68 @@ 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 AgentRegistryLiveTargetEntryLastTerminalEvent 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, AgentRegistryLiveTargetEntryLastTerminalEvent value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SlashCommandInputCompletion)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryLastTerminalEvent)); } } } -/// Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command. +/// Coarse lifecycle status of the foreground session. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SlashCommandKind : 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 SlashCommandKind(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; - /// Command implemented by the runtime. - public static SlashCommandKind Builtin { get; } = new("builtin"); + /// Session is actively processing a turn. + public static AgentRegistryLiveTargetEntryStatus Working { get; } = new("working"); - /// Command backed by a skill. - public static SlashCommandKind Skill { get; } = new("skill"); + /// Session is idle, waiting for input. + public static AgentRegistryLiveTargetEntryStatus Waiting { get; } = new("waiting"); - /// Command registered by an SDK client or extension. - public static SlashCommandKind Client { get; } = new("client"); + /// Last turn completed successfully. + public static AgentRegistryLiveTargetEntryStatus Done { get; } = new("done"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SlashCommandKind left, SlashCommandKind right) => left.Equals(right); + /// 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 not equivalent. - public static bool operator !=(SlashCommandKind left, SlashCommandKind right) => !(left == 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 !=(AgentRegistryLiveTargetEntryStatus left, AgentRegistryLiveTargetEntryStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SlashCommandKind other && Equals(other); + public override bool Equals(object? obj) => obj is AgentRegistryLiveTargetEntryStatus other && Equals(other); /// - public bool Equals(SlashCommandKind 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); @@ -29985,62 +32600,65 @@ 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 AgentRegistryLiveTargetEntryStatus 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, AgentRegistryLiveTargetEntryStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SlashCommandKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryStatus)); } } } -/// Path conventions used by this filesystem. +/// Categorized reason no canonical process log could be opened. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SessionFsSetProviderConventions : 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 SessionFsSetProviderConventions(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; - /// Paths use Windows path conventions. - public static SessionFsSetProviderConventions Windows { get; } = new("windows"); + /// Filesystem permission denied opening the log file. + public static AgentRegistryLogCaptureOpenErrorReason Permission { get; } = new("permission"); - /// Paths use POSIX path conventions. - public static SessionFsSetProviderConventions Posix { get; } = new("posix"); + /// No space left on device. + public static AgentRegistryLogCaptureOpenErrorReason DiskFull { get; } = new("disk_full"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SessionFsSetProviderConventions left, SessionFsSetProviderConventions right) => left.Equals(right); + /// Other / uncategorized open failure. + public static AgentRegistryLogCaptureOpenErrorReason Other { get; } = new("other"); - /// 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 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 !=(AgentRegistryLogCaptureOpenErrorReason left, AgentRegistryLogCaptureOpenErrorReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SessionFsSetProviderConventions other && Equals(other); + public override bool Equals(object? obj) => obj is AgentRegistryLogCaptureOpenErrorReason other && Equals(other); /// - public bool Equals(SessionFsSetProviderConventions 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); @@ -30048,62 +32666,71 @@ 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 AgentRegistryLogCaptureOpenErrorReason 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, AgentRegistryLogCaptureOpenErrorReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionFsSetProviderConventions)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLogCaptureOpenErrorReason)); } } } -/// Repository host type. +/// 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 SessionContextHostType : 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 SessionContextHostType(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; - /// Session repository is hosted on GitHub. - public static SessionContextHostType GitHub { get; } = new("github"); + /// The cwd parameter. + public static AgentRegistrySpawnValidationErrorField Cwd { get; } = new("cwd"); - /// Session repository is hosted on Azure DevOps. - public static SessionContextHostType Ado { get; } = new("ado"); + /// The session name parameter. + public static AgentRegistrySpawnValidationErrorField Name { get; } = new("name"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SessionContextHostType left, SessionContextHostType 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 !=(SessionContextHostType left, SessionContextHostType 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 SessionContextHostType other && Equals(other); + public override bool Equals(object? obj) => obj is AgentRegistrySpawnValidationErrorField other && Equals(other); /// - public bool Equals(SessionContextHostType 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); @@ -30111,68 +32738,74 @@ 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 AgentRegistrySpawnValidationErrorField 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, AgentRegistrySpawnValidationErrorField value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionContextHostType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnValidationErrorField)); } } } -/// 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. +/// 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 RemoteSessionHostStatus : 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 RemoteSessionHostStatus(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; - /// No turn is running. - public static RemoteSessionHostStatus Idle { get; } = new("idle"); + /// Provided cwd does not exist on disk. + public static AgentRegistrySpawnValidationErrorReason CwdNotFound { get; } = new("cwd-not-found"); - /// A turn is running. - public static RemoteSessionHostStatus Working { get; } = new("working"); + /// Provided cwd exists but is not a directory. + public static AgentRegistrySpawnValidationErrorReason CwdNotDirectory { get; } = new("cwd-not-directory"); - /// The session is blocked on the user: a permission prompt or an `ask_user` question. - public static RemoteSessionHostStatus InputNeeded { get; } = new("input-needed"); + /// Session name failed validateSessionName. + public static AgentRegistrySpawnValidationErrorReason InvalidName { get; } = new("invalid-name"); - /// The session ended its last turn in an error. - public static RemoteSessionHostStatus Error { get; } = new("error"); + /// 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 equivalent. - public static bool operator ==(RemoteSessionHostStatus left, RemoteSessionHostStatus right) => left.Equals(right); + /// Requested model is not available to this session. + public static AgentRegistrySpawnValidationErrorReason UnknownModel { get; } = new("unknown-model"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(RemoteSessionHostStatus left, RemoteSessionHostStatus right) => !(left == right); + /// 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 RemoteSessionHostStatus other && Equals(other); + public override bool Equals(object? obj) => obj is AgentRegistrySpawnValidationErrorReason other && Equals(other); /// - public bool Equals(RemoteSessionHostStatus 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); @@ -30180,62 +32813,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 AgentRegistrySpawnValidationErrorReason 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, AgentRegistrySpawnValidationErrorReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(RemoteSessionHostStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnValidationErrorReason)); } } } -/// Whether the remote task originated from CCA or CLI `--remote`. +/// 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 RemoteSessionMetadataTaskType : 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 RemoteSessionMetadataTaskType(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; - /// GitHub Copilot coding agent task. - public static RemoteSessionMetadataTaskType Cca { get; } = new("cca"); + /// Standard permission posture (prompts for each request). + public static AgentRegistrySpawnPermissionMode Default { get; } = new("default"); - /// CLI remote task. - public static RemoteSessionMetadataTaskType Cli { get; } = new("cli"); + /// 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 ==(RemoteSessionMetadataTaskType left, RemoteSessionMetadataTaskType 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 !=(RemoteSessionMetadataTaskType left, RemoteSessionMetadataTaskType 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 RemoteSessionMetadataTaskType other && Equals(other); + public override bool Equals(object? obj) => obj is AgentRegistrySpawnPermissionMode other && Equals(other); /// - public bool Equals(RemoteSessionMetadataTaskType 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); @@ -30243,62 +32876,68 @@ 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 AgentRegistrySpawnPermissionMode 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, AgentRegistrySpawnPermissionMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(RemoteSessionMetadataTaskType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnPermissionMode)); } } } -/// Step status. +/// 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 SessionsOpenProgressStatus : 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 SessionsOpenProgressStatus(string value) + public SendAgentMode(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"); + /// The agent is responding interactively to the user. + public static SendAgentMode Interactive { get; } = new("interactive"); - /// The step has completed successfully. - public static SessionsOpenProgressStatus Complete { get; } = new("complete"); + /// The agent is preparing a plan before making changes. + public static SendAgentMode Plan { get; } = new("plan"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SessionsOpenProgressStatus left, SessionsOpenProgressStatus right) => left.Equals(right); + /// The agent is working autonomously toward task completion. + public static SendAgentMode Autopilot { get; } = new("autopilot"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SessionsOpenProgressStatus left, SessionsOpenProgressStatus right) => !(left == right); + /// 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 ==(SendAgentMode left, SendAgentMode right) => left.Equals(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 SessionsOpenProgressStatus other && Equals(other); + public override bool Equals(object? obj) => obj is SendAgentMode other && Equals(other); /// - public bool Equals(SessionsOpenProgressStatus 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); @@ -30306,74 +32945,62 @@ 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 SendAgentMode 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, SendAgentMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionsOpenProgressStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SendAgentMode)); } } } -/// Handoff step. +/// 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 SessionsOpenProgressStep : 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 SessionsOpenProgressStep(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; - /// 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"); - - /// 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"); + /// Append the message to the normal session queue. + public static SendMode Enqueue { get; } = new("enqueue"); - /// Persisting the newly-created local session to disk. - public static SessionsOpenProgressStep SaveSession { get; } = new("save-session"); + /// 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 ==(SessionsOpenProgressStep left, SessionsOpenProgressStep 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 !=(SessionsOpenProgressStep left, SessionsOpenProgressStep 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 SessionsOpenProgressStep other && Equals(other); + public override bool Equals(object? obj) => obj is SendMode other && Equals(other); /// - public bool Equals(SessionsOpenProgressStep 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); @@ -30381,71 +33008,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 SendMode 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, SendMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionsOpenProgressStep)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SendMode)); } } } -/// Outcome of the open request. +/// 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 SessionsOpenStatus : 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 SessionsOpenStatus(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; - /// 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"); + /// Informational message. + public static SessionLogLevel Info { get; } = new("info"); - /// Connected to an existing remote session. - public static SessionsOpenStatus Connected { get; } = new("connected"); + /// Warning message that may require attention. + public static SessionLogLevel Warning { get; } = new("warning"); - /// Remote session was handed off to a new local session. - public static SessionsOpenStatus HandedOff { get; } = new("handed_off"); + /// 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 ==(SessionsOpenStatus left, SessionsOpenStatus 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 !=(SessionsOpenStatus left, SessionsOpenStatus 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 SessionsOpenStatus other && Equals(other); + public override bool Equals(object? obj) => obj is SessionLogLevel other && Equals(other); /// - public bool Equals(SessionsOpenStatus 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); @@ -30453,62 +33074,65 @@ 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 SessionLogLevel 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, SessionLogLevel value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionsOpenStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionLogLevel)); } } } -/// Neutral SDK discriminator for the connected remote session kind. +/// 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 ConnectedRemoteSessionMetadataKind : 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 ConnectedRemoteSessionMetadataKind(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; - /// Remote CLI session. - public static ConnectedRemoteSessionMetadataKind RemoteSession { get; } = new("remote-session"); + /// The adapter declares that automatic discovery is safe when the other policy fields are satisfied. + public static ModelProviderAutomaticDiscoveryMode Automatic { get; } = new("automatic"); - /// GitHub Copilot coding agent session. - public static ConnectedRemoteSessionMetadataKind CodingAgent { get; } = new("coding-agent"); + /// The adapter may refresh instances the user already configured, but must not scan for new instances automatically. + public static ModelProviderAutomaticDiscoveryMode ConfiguredOnly { get; } = new("configuredOnly"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ConnectedRemoteSessionMetadataKind left, ConnectedRemoteSessionMetadataKind right) => left.Equals(right); + /// 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 not equivalent. - public static bool operator !=(ConnectedRemoteSessionMetadataKind left, ConnectedRemoteSessionMetadataKind right) => !(left == 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 !=(ModelProviderAutomaticDiscoveryMode left, ModelProviderAutomaticDiscoveryMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ConnectedRemoteSessionMetadataKind other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderAutomaticDiscoveryMode other && Equals(other); /// - public bool Equals(ConnectedRemoteSessionMetadataKind 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); @@ -30516,65 +33140,71 @@ 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 ModelProviderAutomaticDiscoveryMode 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, ModelProviderAutomaticDiscoveryMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectedRemoteSessionMetadataKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderAutomaticDiscoveryMode)); } } } -/// Which session sources to include. Defaults to `local` for backward compatibility. +/// Network reach an adapter may use during discovery. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SessionSource : 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 SessionSource(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; - /// Return only local sessions. - public static SessionSource Local { get; } = new("local"); + /// Discovery does not contact a network service. + public static ModelProviderDiscoveryNetworkScope None { get; } = new("none"); - /// Return only remote sessions. - public static SessionSource Remote { get; } = new("remote"); + /// Discovery is limited to loopback addresses on the local machine. + public static ModelProviderDiscoveryNetworkScope LoopbackOnly { get; } = new("loopbackOnly"); - /// Return both local and remote sessions. - public static SessionSource All { get; } = new("all"); + /// Discovery contacts only endpoints the user already configured. + public static ModelProviderDiscoveryNetworkScope ConfiguredEndpointOnly { get; } = new("configuredEndpointOnly"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SessionSource left, SessionSource right) => left.Equals(right); + /// Discovery may scan or contact the local network. + public static ModelProviderDiscoveryNetworkScope LocalNetwork { get; } = new("localNetwork"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(SessionSource left, SessionSource right) => !(left == right); + /// 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 ==(ModelProviderDiscoveryNetworkScope left, ModelProviderDiscoveryNetworkScope right) => left.Equals(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 SessionSource other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderDiscoveryNetworkScope other && Equals(other); /// - public bool Equals(SessionSource 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); @@ -30582,62 +33212,68 @@ 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 ModelProviderDiscoveryNetworkScope 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, ModelProviderDiscoveryNetworkScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderDiscoveryNetworkScope)); } } } -/// 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. +/// 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 EventsCursorStatus : 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 EventsCursorStatus(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; - /// The read succeeded against the requested history. - public static EventsCursorStatus Ok { get; } = new("ok"); + /// Built into the runtime. + public static ModelProviderProvenanceSource BuiltIn { get; } = new("builtIn"); - /// The requested continuation is unavailable; see the endpoint's recovery semantics. - public static EventsCursorStatus Expired { get; } = new("expired"); + /// 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 ==(EventsCursorStatus left, EventsCursorStatus 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 !=(EventsCursorStatus left, EventsCursorStatus 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 EventsCursorStatus other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderProvenanceSource other && Equals(other); /// - public bool Equals(EventsCursorStatus 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); @@ -30645,62 +33281,62 @@ 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 ModelProviderProvenanceSource 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, ModelProviderProvenanceSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsCursorStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderProvenanceSource)); } } } -/// 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. +/// Transport to be used for provider requests. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct EventsReadDirection : 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 EventsReadDirection(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; - /// Page from the cursor toward newer events (default). - public static EventsReadDirection Forward { get; } = new("forward"); + /// HTTP request/streaming transport. + public static ProviderEndpointTransport Http { get; } = new("http"); - /// Tail-first: return the newest events and page toward older events. - public static EventsReadDirection Backward { get; } = new("backward"); + /// WebSocket transport. + public static ProviderEndpointTransport Websockets { get; } = new("websockets"); - /// 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 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 !=(EventsReadDirection left, EventsReadDirection 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 EventsReadDirection other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderEndpointTransport other && Equals(other); /// - public bool Equals(EventsReadDirection 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); @@ -30708,71 +33344,65 @@ 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 ProviderEndpointTransport 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, ProviderEndpointTransport value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsReadDirection)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointTransport)); } } } -/// Kind of attention required when status === "attention". Meaningful only when status === "attention". +/// 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 AgentRegistryLiveTargetEntryAttentionKind : 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 AgentRegistryLiveTargetEntryAttentionKind(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; - /// Session is blocked on an unrecoverable error. - public static AgentRegistryLiveTargetEntryAttentionKind Error { get; } = new("error"); - - /// Session is waiting for a tool-permission decision. - public static AgentRegistryLiveTargetEntryAttentionKind Permission { get; } = new("permission"); - - /// Session is waiting for the user to approve or reject a plan. - public static AgentRegistryLiveTargetEntryAttentionKind ExitPlan { get; } = new("exit_plan"); + /// OpenAI-compatible endpoint (use the OpenAI client library). + public static ProviderEndpointType Openai { get; } = new("openai"); - /// Session is waiting on an elicitation prompt. - public static AgentRegistryLiveTargetEntryAttentionKind Elicitation { get; } = new("elicitation"); + /// Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). + public static ProviderEndpointType Azure { get; } = new("azure"); - /// Session is waiting for free-form user input. - public static AgentRegistryLiveTargetEntryAttentionKind UserInput { get; } = new("user_input"); + /// 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 ==(AgentRegistryLiveTargetEntryAttentionKind left, AgentRegistryLiveTargetEntryAttentionKind 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 !=(AgentRegistryLiveTargetEntryAttentionKind left, AgentRegistryLiveTargetEntryAttentionKind 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 AgentRegistryLiveTargetEntryAttentionKind other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderEndpointType other && Equals(other); /// - public bool Equals(AgentRegistryLiveTargetEntryAttentionKind 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); @@ -30780,62 +33410,62 @@ 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 ProviderEndpointType 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, ProviderEndpointType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryAttentionKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointType)); } } } -/// Process kind tag for the registry entry. +/// 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 AgentRegistryLiveTargetEntryKind : 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 AgentRegistryLiveTargetEntryKind(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; - /// Interactive Copilot CLI exposing a UI server (legacy/normal CLI process). - public static AgentRegistryLiveTargetEntryKind UiServer { get; } = new("ui-server"); + /// Classic chat-completions request shape. + public static ProviderEndpointWireApi Completions { get; } = new("completions"); - /// Headless `--server --managed-server` child spawned by a controller. - public static AgentRegistryLiveTargetEntryKind ManagedServer { get; } = new("managed-server"); + /// Newer responses request shape. + public static ProviderEndpointWireApi Responses { get; } = new("responses"); - /// 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 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 !=(AgentRegistryLiveTargetEntryKind left, AgentRegistryLiveTargetEntryKind 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 AgentRegistryLiveTargetEntryKind other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderEndpointWireApi other && Equals(other); /// - public bool Equals(AgentRegistryLiveTargetEntryKind 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); @@ -30843,62 +33473,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 ProviderEndpointWireApi 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, ProviderEndpointWireApi value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointWireApi)); } } } -/// How the most recent turn ended (clean vs aborted). Lets the renderer distinguish done from done_cancelled. +/// Typed outcome for a provider operation. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistryLiveTargetEntryLastTerminalEvent : 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 AgentRegistryLiveTargetEntryLastTerminalEvent(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; - /// Last turn ended cleanly (model returned a final assistant message). - public static AgentRegistryLiveTargetEntryLastTerminalEvent TurnEnd { get; } = new("turn_end"); + /// The operation completed successfully; an empty inventory is valid. + public static ModelProviderOperationOutcomeCode Success { get; } = new("success"); - /// Last turn was aborted (e.g. user interrupted). - public static AgentRegistryLiveTargetEntryLastTerminalEvent Abort { get; } = new("abort"); + /// 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"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(AgentRegistryLiveTargetEntryLastTerminalEvent left, AgentRegistryLiveTargetEntryLastTerminalEvent right) => left.Equals(right); + /// 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 not equivalent. - public static bool operator !=(AgentRegistryLiveTargetEntryLastTerminalEvent left, AgentRegistryLiveTargetEntryLastTerminalEvent right) => !(left == 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 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 AgentRegistryLiveTargetEntryLastTerminalEvent other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderOperationOutcomeCode other && Equals(other); /// - public bool Equals(AgentRegistryLiveTargetEntryLastTerminalEvent 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); @@ -30906,68 +33542,59 @@ 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 ModelProviderOperationOutcomeCode 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, ModelProviderOperationOutcomeCode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryLastTerminalEvent)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderOperationOutcomeCode)); } } } -/// Coarse lifecycle status of the foreground session. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistryLiveTargetEntryStatus : 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 AgentRegistryLiveTargetEntryStatus(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; - /// 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"); - - /// Session needs user attention (see attentionKind for the specific reason). - public static AgentRegistryLiveTargetEntryStatus Attention { get; } = new("attention"); + /// Gets the append value. + public static ProtocolAppendMode Append { get; } = new("append"); - /// 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 ==(ProtocolAppendMode left, ProtocolAppendMode 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 !=(ProtocolAppendMode left, ProtocolAppendMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistryLiveTargetEntryStatus other && Equals(other); + public override bool Equals(object? obj) => obj is ProtocolAppendMode other && Equals(other); /// - public bool Equals(AgentRegistryLiveTargetEntryStatus 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); @@ -30975,65 +33602,59 @@ 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 ProtocolAppendMode 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, ProtocolAppendMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLiveTargetEntryStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolAppendMode)); } } } -/// Categorized reason no canonical process log could be opened. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistryLogCaptureOpenErrorReason : 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 AgentRegistryLogCaptureOpenErrorReason(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; - /// Filesystem permission denied opening the log file. - public static AgentRegistryLogCaptureOpenErrorReason Permission { get; } = new("permission"); - - /// No space left on device. - public static AgentRegistryLogCaptureOpenErrorReason DiskFull { get; } = new("disk_full"); - - /// Other / uncategorized open failure. - public static AgentRegistryLogCaptureOpenErrorReason Other { get; } = new("other"); + /// Gets the replace value. + public static ProtocolReplaceMode Replace { get; } = new("replace"); - /// 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 ==(ProtocolReplaceMode left, ProtocolReplaceMode 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 !=(ProtocolReplaceMode left, ProtocolReplaceMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistryLogCaptureOpenErrorReason other && Equals(other); + public override bool Equals(object? obj) => obj is ProtocolReplaceMode other && Equals(other); /// - public bool Equals(AgentRegistryLogCaptureOpenErrorReason 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); @@ -31041,71 +33662,59 @@ 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 ProtocolReplaceMode 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, ProtocolReplaceMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistryLogCaptureOpenErrorReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolReplaceMode)); } } } -/// Which parameter field was invalid. Omitted when the rejection is not field-specific. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistrySpawnValidationErrorField : 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 AgentRegistrySpawnValidationErrorField(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 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 model parameter. - public static AgentRegistrySpawnValidationErrorField Model { get; } = new("model"); - - /// The permissionMode parameter. - public static AgentRegistrySpawnValidationErrorField PermissionMode { get; } = new("permissionMode"); + /// Gets the customize value. + public static ProtocolCustomizeMode Customize { get; } = new("customize"); - /// 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 ==(ProtocolCustomizeMode left, ProtocolCustomizeMode 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 !=(ProtocolCustomizeMode left, ProtocolCustomizeMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is AgentRegistrySpawnValidationErrorField other && Equals(other); + public override bool Equals(object? obj) => obj is ProtocolCustomizeMode other && Equals(other); /// - public bool Equals(AgentRegistrySpawnValidationErrorField 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); @@ -31113,74 +33722,68 @@ 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 ProtocolCustomizeMode 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, ProtocolCustomizeMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnValidationErrorField)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolCustomizeMode)); } } } -/// Categorized reason for the rejection. Low-cardinality enum so telemetry can aggregate by reason without leaking raw paths or agent/model names. +/// Defines the allowed values. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AgentRegistrySpawnValidationErrorReason : 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 AgentRegistrySpawnValidationErrorReason(string value) + public ProtocolStaticSectionAction(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"); - - /// Session name failed validateSessionName. - public static AgentRegistrySpawnValidationErrorReason InvalidName { get; } = new("invalid-name"); + /// Replace the section content. + public static ProtocolStaticSectionAction Replace { get; } = new("replace"); - /// Requested agent name was not found in builtin or custom agents. - public static AgentRegistrySpawnValidationErrorReason UnknownAgent { get; } = new("unknown-agent"); + /// Remove the section content. + public static ProtocolStaticSectionAction Remove { get; } = new("remove"); - /// Requested model is not available to this session. - public static AgentRegistrySpawnValidationErrorReason UnknownModel { get; } = new("unknown-model"); + /// Append content to the section. + public static ProtocolStaticSectionAction Append { get; } = new("append"); - /// Caller asked for permissionMode='yolo' but the controller is not currently in allow-all mode. - public static AgentRegistrySpawnValidationErrorReason YoloNotAllowed { get; } = new("yolo-not-allowed"); + /// 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 ==(AgentRegistrySpawnValidationErrorReason left, AgentRegistrySpawnValidationErrorReason 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 !=(AgentRegistrySpawnValidationErrorReason left, AgentRegistrySpawnValidationErrorReason 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 AgentRegistrySpawnValidationErrorReason other && Equals(other); + public override bool Equals(object? obj) => obj is ProtocolStaticSectionAction other && Equals(other); /// - public bool Equals(AgentRegistrySpawnValidationErrorReason 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); @@ -31188,62 +33791,62 @@ 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 ProtocolStaticSectionAction 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, ProtocolStaticSectionAction value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnValidationErrorReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolStaticSectionAction)); } } } -/// Permission posture for the new session. 'yolo' requires the controller-local session to currently be in allow-all mode. +/// 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 AgentRegistrySpawnPermissionMode : 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 AgentRegistrySpawnPermissionMode(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; - /// Standard permission posture (prompts for each request). - public static AgentRegistrySpawnPermissionMode Default { get; } = new("default"); + /// No matching entry is registered; the caller should add the entry. + public static ModelProviderConfigurationDisposition Create { get; } = new("create"); - /// Full allow-all (requires the controller-local session to currently be in allow-all mode). - public static AgentRegistrySpawnPermissionMode Yolo { get; } = new("yolo"); + /// 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 ==(AgentRegistrySpawnPermissionMode left, AgentRegistrySpawnPermissionMode 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 !=(AgentRegistrySpawnPermissionMode left, AgentRegistrySpawnPermissionMode 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 AgentRegistrySpawnPermissionMode other && Equals(other); + public override bool Equals(object? obj) => obj is ModelProviderConfigurationDisposition other && Equals(other); /// - public bool Equals(AgentRegistrySpawnPermissionMode 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); @@ -31251,65 +33854,77 @@ 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 ModelProviderConfigurationDisposition 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, ModelProviderConfigurationDisposition value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentRegistrySpawnPermissionMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelProviderConfigurationDisposition)); } } } -/// How far OneAuth may go to acquire the requested token. +/// 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 EntraTokenInteraction : 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 EntraTokenInteraction(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; - /// Acquire the token without any user interaction, failing if interaction would be required. - public static EntraTokenInteraction Silent { get; } = new("silent"); + /// OpenAI API. + public static ProviderConfigModelProvider Openai { get; } = new("openai"); + + /// Anthropic API. + public static ProviderConfigModelProvider Anthropic { get; } = new("anthropic"); + + /// Azure OpenAI Service. + public static ProviderConfigModelProvider AzureOpenai { get; } = new("azure_openai"); - /// Allow interactive acquisition, prompting the user only when a cached or silent token is unavailable. - public static EntraTokenInteraction Interactive { get; } = new("interactive"); + /// Ollama. + public static ProviderConfigModelProvider Ollama { get; } = new("ollama"); - /// Always prompt interactively, bypassing any cached or silently-refreshable token. - public static EntraTokenInteraction ForceInteractive { get; } = new("force-interactive"); + /// LM Studio. + public static ProviderConfigModelProvider LmStudio { get; } = new("lm_studio"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(EntraTokenInteraction left, EntraTokenInteraction right) => left.Equals(right); + /// Foundry Local. + public static ProviderConfigModelProvider FoundryLocal { get; } = new("foundry_local"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(EntraTokenInteraction left, EntraTokenInteraction right) => !(left == right); + /// 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 EntraTokenInteraction other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderConfigModelProvider other && Equals(other); /// - public bool Equals(EntraTokenInteraction 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); @@ -31317,68 +33932,62 @@ 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 ProviderConfigModelProvider 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, ProviderConfigModelProvider value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EntraTokenInteraction)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigModelProvider)); } } } -/// The UI mode the agent was in when this message was sent. Defaults to the session's current mode. +/// Provider transport. Defaults to "http". [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SendAgentMode : 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 SendAgentMode(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 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"); - - /// The agent is working autonomously toward task completion. - public static SendAgentMode Autopilot { get; } = new("autopilot"); + /// HTTP request/streaming transport. + public static ProviderConfigTransport Http { get; } = new("http"); - /// The agent is in shell-focused UI mode. - public static SendAgentMode Shell { get; } = new("shell"); + /// WebSocket transport. + public static ProviderConfigTransport Websockets { get; } = new("websockets"); - /// 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 ==(ProviderConfigTransport left, ProviderConfigTransport 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 !=(ProviderConfigTransport left, ProviderConfigTransport right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SendAgentMode other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderConfigTransport other && Equals(other); /// - public bool Equals(SendAgentMode 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); @@ -31386,62 +33995,65 @@ 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 ProviderConfigTransport 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, ProviderConfigTransport value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SendAgentMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigTransport)); } } } -/// How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. +/// 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 SendMode : 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 SendMode(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; - /// Append the message to the normal session queue. - public static SendMode Enqueue { get; } = new("enqueue"); + /// Generic OpenAI-compatible API. + public static ProviderConfigType Openai { get; } = new("openai"); - /// Interject the message during the in-progress turn. - public static SendMode Immediate { get; } = new("immediate"); + /// Azure OpenAI Service endpoint. + public static ProviderConfigType Azure { get; } = new("azure"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(SendMode left, SendMode right) => left.Equals(right); + /// Anthropic API endpoint. + public static ProviderConfigType Anthropic { get; } = new("anthropic"); - /// 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 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 !=(ProviderConfigType left, ProviderConfigType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is SendMode other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderConfigType other && Equals(other); /// - public bool Equals(SendMode 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); @@ -31449,65 +34061,62 @@ 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 ProviderConfigType 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, ProviderConfigType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SendMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigType)); } } } -/// Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". +/// Wire API format (openai/azure only). Defaults to "completions". [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct SessionLogLevel : 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 SessionLogLevel(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; - /// Informational message. - public static SessionLogLevel Info { get; } = new("info"); - - /// Warning message that may require attention. - public static SessionLogLevel Warning { get; } = new("warning"); + /// OpenAI Chat Completions wire format. + public static ProviderConfigWireApi Completions { get; } = new("completions"); - /// Error message describing a failure. - public static SessionLogLevel Error { get; } = new("error"); + /// 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 ==(SessionLogLevel left, SessionLogLevel 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 !=(SessionLogLevel left, SessionLogLevel 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 SessionLogLevel other && Equals(other); + public override bool Equals(object? obj) => obj is ProviderConfigWireApi other && Equals(other); /// - public bool Equals(SessionLogLevel 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); @@ -31515,20 +34124,20 @@ 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 ProviderConfigWireApi 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, ProviderConfigWireApi value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(SessionLogLevel)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigWireApi)); } } } @@ -31767,6 +34376,9 @@ public AuthInfoType(string value) /// Authentication from an interactive user sign-in. public static AuthInfoType User { get; } = new("user"); + /// Authentication from a selected provider-owned account, without a GitHub credential. + public static AuthInfoType Account { get; } = new("account"); + /// Authentication delegated to the GitHub CLI. public static AuthInfoType GhCli { get; } = new("gh-cli"); @@ -31957,7 +34569,7 @@ public override void Write(Utf8JsonWriter writer, LoginProviderKind value, JsonS } -/// Terminal disposition of a login persistence attempt. +/// Disposition of a login attempt, including pending user decisions. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -31977,12 +34589,15 @@ public AuthLoginResultStatus(string value) /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// The credential was persisted and the account is signed in. + /// The credential was persisted and the selected account is signed in. public static AuthLoginResultStatus Completed { get; } = new("completed"); /// Persistence needs explicit consent to store the token in plaintext. public static AuthLoginResultStatus NeedsPlaintextConsent { get; } = new("needs-plaintext-consent"); + /// Credentials are saved; select an account using a returned selectionId as advance input to complete sign-in. + public static AuthLoginResultStatus NeedsAccountSelection { get; } = new("needs-account-selection"); + /// The user declined plaintext persistence. public static AuthLoginResultStatus Declined { get; } = new("declined"); @@ -32510,510 +35125,57 @@ public override WorkflowRunStatus Read(ref Utf8JsonReader reader, Type typeToCon } /// - public override void Write(Utf8JsonWriter writer, WorkflowRunStatus value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowRunStatus)); - } - } -} - - -/// Derived lifecycle state of a workflow phase. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct WorkflowPhaseStatus : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public WorkflowPhaseStatus(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// 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 phase is currently entered and accumulating active time. - public static WorkflowPhaseStatus Active { get; } = new("active"); - - /// The phase was entered and has since been closed. - public static WorkflowPhaseStatus Completed { get; } = new("completed"); - - /// 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 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 WorkflowPhaseStatus other && Equals(other); - - /// - public bool Equals(WorkflowPhaseStatus 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 WorkflowPhaseStatus 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) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowPhaseStatus)); - } - } -} - - -/// Kind of workflow progress line. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct WorkflowLogLineKind : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public WorkflowLogLineKind(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// A narrator log line. - public static WorkflowLogLineKind Log { get; } = new("log"); - - /// 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 ==(WorkflowLogLineKind left, WorkflowLogLineKind right) => left.Equals(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 WorkflowLogLineKind other && Equals(other); - - /// - public bool Equals(WorkflowLogLineKind 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 WorkflowLogLineKind 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) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowLogLineKind)); - } - } -} - - -/// 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 WorkflowPauseCheckpointAction : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public WorkflowPauseCheckpointAction(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// 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"); - - /// 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 ==(WorkflowPauseCheckpointAction left, WorkflowPauseCheckpointAction right) => left.Equals(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 WorkflowPauseCheckpointAction other && Equals(other); - - /// - public bool Equals(WorkflowPauseCheckpointAction 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 WorkflowPauseCheckpointAction 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) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowPauseCheckpointAction)); - } - } -} - - -/// 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 ModelSwitchAutoTierStatus : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public ModelSwitchAutoTierStatus(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// 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"); - - /// 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 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 ModelSwitchAutoTierStatus other && Equals(other); - - /// - public bool Equals(ModelSwitchAutoTierStatus 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 ModelSwitchAutoTierStatus 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) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelSwitchAutoTierStatus)); - } - } -} - - -/// Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct WorkspacesWorkspaceDetailsHostType : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public WorkspacesWorkspaceDetailsHostType(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// 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"); - - /// 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 ==(WorkspacesWorkspaceDetailsHostType left, WorkspacesWorkspaceDetailsHostType right) => left.Equals(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 WorkspacesWorkspaceDetailsHostType other && Equals(other); - - /// - public bool Equals(WorkspacesWorkspaceDetailsHostType 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 WorkspacesWorkspaceDetailsHostType 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) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspacesWorkspaceDetailsHostType)); - } - } -} - - -/// Type of change represented by this file diff. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct WorkspaceDiffFileChangeType : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public WorkspaceDiffFileChangeType(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// 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 file was deleted. - public static WorkspaceDiffFileChangeType Deleted { get; } = new("deleted"); - - /// 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 WorkspaceDiffFileChangeType other && Equals(other); - - /// - public bool Equals(WorkspaceDiffFileChangeType 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 WorkspaceDiffFileChangeType 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) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspaceDiffFileChangeType)); - } - } -} - - -/// Diff mode requested by the client. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct WorkspaceDiffMode : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public WorkspaceDiffMode(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// 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"); - - /// Return changes compared with the default branch. - public static WorkspaceDiffMode Branch { get; } = new("branch"); - - /// 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 ==(WorkspaceDiffMode left, WorkspaceDiffMode right) => left.Equals(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 WorkspaceDiffMode other && Equals(other); - - /// - public bool Equals(WorkspaceDiffMode 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 WorkspaceDiffMode 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, WorkflowRunStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspaceDiffMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowRunStatus)); } } } -/// Reason a rewind read (rewind points, file-restore preview, or session diff) could not be answered from the session's file-change captures. +/// Derived lifecycle state of a workflow phase. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct HistoryRewindUnavailableReason : 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 HistoryRewindUnavailableReason(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 session did not opt into file-change tracking before its first turn. - public static HistoryRewindUnavailableReason FileChangeTrackingDisabled { get; } = new("file-change-tracking-disabled"); + /// The phase has not been entered yet. + public static WorkflowPhaseStatus 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 phase is currently entered and accumulating active time. + public static WorkflowPhaseStatus Active { get; } = new("active"); - /// Remote-backed rewind routing is not supported. - public static HistoryRewindUnavailableReason UnsupportedRemoteSession { get; } = new("unsupported-remote-session"); + /// 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 ==(HistoryRewindUnavailableReason left, HistoryRewindUnavailableReason 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 !=(HistoryRewindUnavailableReason left, HistoryRewindUnavailableReason 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 HistoryRewindUnavailableReason other && Equals(other); + public override bool Equals(object? obj) => obj is WorkflowPhaseStatus other && Equals(other); /// - public bool Equals(HistoryRewindUnavailableReason 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); @@ -33021,65 +35183,62 @@ 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 WorkflowPhaseStatus 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, WorkflowPhaseStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HistoryRewindUnavailableReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowPhaseStatus)); } } } -/// Current normalized autopilot objective lifecycle status. +/// Kind of workflow progress line. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct AutopilotObjectiveStatus : 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 AutopilotObjectiveStatus(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 objective is actively running. - public static AutopilotObjectiveStatus Active { get; } = new("active"); - - /// The objective is paused and may be resumed. - public static AutopilotObjectiveStatus Paused { get; } = new("paused"); + /// A narrator log line. + public static WorkflowLogLineKind Log { get; } = new("log"); - /// The objective completed. - public static AutopilotObjectiveStatus Completed { get; } = new("completed"); + /// 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 ==(AutopilotObjectiveStatus left, AutopilotObjectiveStatus 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 !=(AutopilotObjectiveStatus left, AutopilotObjectiveStatus 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 AutopilotObjectiveStatus other && Equals(other); + public override bool Equals(object? obj) => obj is WorkflowLogLineKind other && Equals(other); /// - public bool Equals(AutopilotObjectiveStatus 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); @@ -33087,62 +35246,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 WorkflowLogLineKind 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, WorkflowLogLineKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AutopilotObjectiveStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowLogLineKind)); } } } -/// Whether task execution is synchronously awaited or managed in the background. +/// 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 TaskExecutionMode : IEquatable +public readonly struct WorkflowPauseCheckpointAction : 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 WorkflowPauseCheckpointAction(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"); + /// The checkpoint was committed by a prior paused attempt, so execution may continue. + public static WorkflowPauseCheckpointAction Continue { get; } = new("continue"); - /// The task is managed in the background. - public static TaskExecutionMode Background { get; } = new("background"); + /// 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 ==(TaskExecutionMode left, TaskExecutionMode 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 !=(TaskExecutionMode left, TaskExecutionMode 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 TaskExecutionMode other && Equals(other); + public override bool Equals(object? obj) => obj is WorkflowPauseCheckpointAction other && Equals(other); /// - public bool Equals(TaskExecutionMode 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); @@ -33150,71 +35309,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 WorkflowPauseCheckpointAction 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, WorkflowPauseCheckpointAction value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskExecutionMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkflowPauseCheckpointAction)); } } } -/// Current lifecycle status of the task. +/// 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 TaskStatus : 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 TaskStatus(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; - /// 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"); - - /// The task finished successfully. - public static TaskStatus Completed { get; } = new("completed"); - - /// The task finished with an error. - public static TaskStatus Failed { get; } = new("failed"); + /// 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"); - /// The task was cancelled before completion. - public static TaskStatus Cancelled { get; } = new("cancelled"); + /// 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 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 ==(ModelSwitchAutoTierStatus left, ModelSwitchAutoTierStatus 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 !=(ModelSwitchAutoTierStatus left, ModelSwitchAutoTierStatus right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskStatus other && Equals(other); + public override bool Equals(object? obj) => obj is ModelSwitchAutoTierStatus other && Equals(other); /// - public bool Equals(TaskStatus 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); @@ -33222,59 +35372,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 ModelSwitchAutoTierStatus 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, ModelSwitchAutoTierStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelSwitchAutoTierStatus)); } } } -/// Client-owned tasks always execute outside the runtime in background mode. +/// Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskClientExecutionMode : 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 TaskClientExecutionMode(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; - /// Gets the background value. - public static TaskClientExecutionMode Background { get; } = new("background"); + /// Workspace repository is hosted on GitHub. + public static WorkspacesWorkspaceDetailsHostType GitHub { get; } = new("github"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskClientExecutionMode left, TaskClientExecutionMode right) => left.Equals(right); + /// Workspace repository is hosted on Azure DevOps. + public static WorkspacesWorkspaceDetailsHostType Ado { get; } = new("ado"); - /// 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 ==(WorkspacesWorkspaceDetailsHostType left, WorkspacesWorkspaceDetailsHostType right) => left.Equals(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 TaskClientExecutionMode other && Equals(other); + public override bool Equals(object? obj) => obj is WorkspacesWorkspaceDetailsHostType other && Equals(other); /// - public bool Equals(TaskClientExecutionMode 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); @@ -33282,62 +35435,68 @@ 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 WorkspacesWorkspaceDetailsHostType 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, WorkspacesWorkspaceDetailsHostType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientExecutionMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspacesWorkspaceDetailsHostType)); } } } -/// Connection class owning a client task. +/// Type of change represented by this file diff. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskClientOwnerKind : 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 TaskClientOwnerKind(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; - /// A discovered extension connection owns the task. - public static TaskClientOwnerKind Extension { get; } = new("extension"); + /// The file was added. + public static WorkspaceDiffFileChangeType Added { get; } = new("added"); - /// A generic SDK connection owns the task. - public static TaskClientOwnerKind Sdk { get; } = new("sdk"); + /// The file was modified. + public static WorkspaceDiffFileChangeType Modified { get; } = new("modified"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskClientOwnerKind left, TaskClientOwnerKind 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 !=(TaskClientOwnerKind left, TaskClientOwnerKind 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 TaskClientOwnerKind other && Equals(other); + public override bool Equals(object? obj) => obj is WorkspaceDiffFileChangeType other && Equals(other); /// - public bool Equals(TaskClientOwnerKind 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); @@ -33345,62 +35504,65 @@ 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 WorkspaceDiffFileChangeType 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, WorkspaceDiffFileChangeType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspaceDiffFileChangeType)); } } } -/// Presence of the task's bound join. +/// Diff mode requested by the client. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskClientOwnerPresence : 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 TaskClientOwnerPresence(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 bound session join is connected. - public static TaskClientOwnerPresence Connected { get; } = new("connected"); + /// Return staged, unstaged, and untracked working tree changes. + public static WorkspaceDiffMode Unstaged { get; } = new("unstaged"); - /// The bound session join is disconnected. - public static TaskClientOwnerPresence Disconnected { get; } = new("disconnected"); + /// Return changes compared with the default branch. + public static WorkspaceDiffMode Branch { get; } = new("branch"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskClientOwnerPresence left, TaskClientOwnerPresence right) => left.Equals(right); + /// 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 not equivalent. - public static bool operator !=(TaskClientOwnerPresence left, TaskClientOwnerPresence right) => !(left == 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 !=(WorkspaceDiffMode left, WorkspaceDiffMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskClientOwnerPresence other && Equals(other); + public override bool Equals(object? obj) => obj is WorkspaceDiffMode other && Equals(other); /// - public bool Equals(TaskClientOwnerPresence 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); @@ -33408,74 +35570,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 WorkspaceDiffMode 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, WorkspaceDiffMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerPresence)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkspaceDiffMode)); } } } -/// Lifecycle status of a client-owned task. +/// 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 TaskClientStatus : 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 TaskClientStatus(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 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"); + /// The session did not opt into file-change tracking before its first turn. + public static HistoryRewindUnavailableReason FileChangeTrackingDisabled { get; } = new("file-change-tracking-disabled"); - /// The owner reported or confirmed cancellation. - public static TaskClientStatus Cancelled { get; } = new("cancelled"); + /// 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 bound owner join disappeared; external executor state is unknown. - public static TaskClientStatus Orphaned { get; } = new("orphaned"); + /// Remote-backed rewind routing is not supported. + public static HistoryRewindUnavailableReason UnsupportedRemoteSession { get; } = new("unsupported-remote-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 ==(HistoryRewindUnavailableReason left, HistoryRewindUnavailableReason 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 !=(HistoryRewindUnavailableReason left, HistoryRewindUnavailableReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is TaskClientStatus other && Equals(other); + public override bool Equals(object? obj) => obj is HistoryRewindUnavailableReason other && Equals(other); /// - public bool Equals(TaskClientStatus 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); @@ -33483,62 +35636,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 HistoryRewindUnavailableReason 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, HistoryRewindUnavailableReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HistoryRewindUnavailableReason)); } } } -/// Whether the shell runs inside a managed PTY session or as an independent background process. +/// Current normalized autopilot objective lifecycle status. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskShellInfoAttachmentMode : 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 TaskShellInfoAttachmentMode(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; - /// The shell runs in a managed PTY session. - public static TaskShellInfoAttachmentMode Attached { get; } = new("attached"); + /// The objective is actively running. + public static AutopilotObjectiveStatus Active { get; } = new("active"); - /// The shell runs as an independent background process. - public static TaskShellInfoAttachmentMode Detached { get; } = new("detached"); + /// The objective is paused and may be resumed. + public static AutopilotObjectiveStatus Paused { get; } = new("paused"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskShellInfoAttachmentMode left, TaskShellInfoAttachmentMode right) => left.Equals(right); + /// The objective completed. + public static AutopilotObjectiveStatus Completed { get; } = new("completed"); - /// 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 ==(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 TaskShellInfoAttachmentMode other && Equals(other); + public override bool Equals(object? obj) => obj is AutopilotObjectiveStatus other && Equals(other); /// - public bool Equals(TaskShellInfoAttachmentMode 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); @@ -33546,59 +35702,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 AutopilotObjectiveStatus 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, AutopilotObjectiveStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskShellInfoAttachmentMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AutopilotObjectiveStatus)); } } } -/// Discriminator for a client-owned task. +/// Result of reloading a customization component. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskClientType : 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 TaskClientType(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; - /// Gets the client value. - public static TaskClientType Client { get; } = new("client"); + /// The component was refreshed successfully. + public static CustomizationReloadStatus Reloaded { get; } = new("reloaded"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(TaskClientType left, TaskClientType right) => left.Equals(right); + /// 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 not equivalent. - public static bool operator !=(TaskClientType left, TaskClientType right) => !(left == 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 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 TaskClientType other && Equals(other); + public override bool Equals(object? obj) => obj is CustomizationReloadStatus other && Equals(other); /// - public bool Equals(TaskClientType 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); @@ -33606,62 +35768,80 @@ 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 CustomizationReloadStatus 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, CustomizationReloadStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CustomizationReloadStatus)); } } } -/// Active status a client owner may publish with a progress update. +/// Component of session customization discovery. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct TaskClientActiveStatus : 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 TaskClientActiveStatus(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 external owner is actively working. - public static TaskClientActiveStatus Running { get; } = new("running"); + /// Repository metadata and working-directory context. + public static CustomizationReloadSubsystem RepositoryContext { get; } = new("repositoryContext"); - /// The external owner is connected but waiting. - public static TaskClientActiveStatus Idle { get; } = new("idle"); + /// 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 ==(TaskClientActiveStatus left, TaskClientActiveStatus 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 !=(TaskClientActiveStatus left, TaskClientActiveStatus 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 TaskClientActiveStatus other && Equals(other); + public override bool Equals(object? obj) => obj is CustomizationReloadSubsystem other && Equals(other); /// - public bool Equals(TaskClientActiveStatus 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); @@ -33669,62 +35849,62 @@ 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 CustomizationReloadSubsystem 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, CustomizationReloadSubsystem value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientActiveStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CustomizationReloadSubsystem)); } } } -/// Consumer allowed to call an MCP tool. +/// 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 McpToolUiVisibility : 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 McpToolUiVisibility(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 model may call the tool. - public static McpToolUiVisibility Model { get; } = new("model"); + /// The task was started with synchronous waiting. + public static TaskExecutionMode Sync { get; } = new("sync"); - /// An MCP App view may call the tool. - public static McpToolUiVisibility App { get; } = new("app"); + /// 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 ==(McpToolUiVisibility left, McpToolUiVisibility 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 !=(McpToolUiVisibility left, McpToolUiVisibility 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 McpToolUiVisibility other && Equals(other); + public override bool Equals(object? obj) => obj is TaskExecutionMode other && Equals(other); /// - public bool Equals(McpToolUiVisibility 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); @@ -33732,65 +35912,71 @@ 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 TaskExecutionMode 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, TaskExecutionMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpToolUiVisibility)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskExecutionMode)); } } } -/// 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. +/// Current lifecycle status of the task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpSamplingExecutionAction : 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 McpSamplingExecutionAction(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; - /// The sampling inference completed and produced a result. - public static McpSamplingExecutionAction Success { get; } = new("success"); + /// The task is actively executing. + public static TaskStatus Running { get; } = new("running"); - /// The sampling inference failed or was rejected. - public static McpSamplingExecutionAction Failure { get; } = new("failure"); + /// The task is waiting for additional input. + public static TaskStatus Idle { get; } = new("idle"); - /// The sampling inference was cancelled before completion. - public static McpSamplingExecutionAction Cancelled { get; } = new("cancelled"); + /// The task finished successfully. + public static TaskStatus Completed { get; } = new("completed"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(McpSamplingExecutionAction left, McpSamplingExecutionAction right) => left.Equals(right); + /// The task finished with an error. + public static TaskStatus Failed { get; } = new("failed"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(McpSamplingExecutionAction left, McpSamplingExecutionAction right) => !(left == right); + /// 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 McpSamplingExecutionAction other && Equals(other); + public override bool Equals(object? obj) => obj is TaskStatus other && Equals(other); /// - public bool Equals(McpSamplingExecutionAction 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); @@ -33798,62 +35984,59 @@ 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 TaskStatus 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, TaskStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpSamplingExecutionAction)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskStatus)); } } } -/// 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". +/// 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 McpSetEnvValueModeDetails : 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 McpSetEnvValueModeDetails(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; - /// Treat MCP server environment values as literal strings. - public static McpSetEnvValueModeDetails Direct { get; } = new("direct"); - - /// Treat MCP server environment values as host-side references to resolve before launch. - public static McpSetEnvValueModeDetails Indirect { get; } = new("indirect"); + /// Gets the background value. + public static TaskClientExecutionMode Background { get; } = new("background"); - /// 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 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 !=(McpSetEnvValueModeDetails left, McpSetEnvValueModeDetails 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 McpSetEnvValueModeDetails other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientExecutionMode other && Equals(other); /// - public bool Equals(McpSetEnvValueModeDetails 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); @@ -33861,62 +36044,62 @@ 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 TaskClientExecutionMode 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, TaskClientExecutionMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpSetEnvValueModeDetails)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientExecutionMode)); } } } -/// Outcome of starting the original prepared owned login. +/// Connection class owning a client task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpOwnedOauthLoginStatus : 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 McpOwnedOauthLoginStatus(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; - /// The original requester may open the returned authorisation URL. - public static McpOwnedOauthLoginStatus AwaitingBrowser { get; } = new("awaiting-browser"); + /// A discovered extension connection owns the task. + public static TaskClientOwnerKind Extension { get; } = new("extension"); - /// Cached credentials were accepted and the original server finished reconnecting. - public static McpOwnedOauthLoginStatus Connected { get; } = new("connected"); + /// 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 ==(McpOwnedOauthLoginStatus left, McpOwnedOauthLoginStatus 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 !=(McpOwnedOauthLoginStatus left, McpOwnedOauthLoginStatus 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 McpOwnedOauthLoginStatus other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientOwnerKind other && Equals(other); /// - public bool Equals(McpOwnedOauthLoginStatus 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); @@ -33924,62 +36107,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 TaskClientOwnerKind 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, TaskClientOwnerKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOwnedOauthLoginStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerKind)); } } } -/// OAuth grant type override for this login. +/// Presence of the task's bound join. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpOauthLoginGrantType : 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 McpOauthLoginGrantType(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; - /// Interactive browser-based OAuth flow using an authorization code, typically with PKCE. - public static McpOauthLoginGrantType AuthorizationCode { get; } = new("authorization_code"); + /// The bound session join is connected. + public static TaskClientOwnerPresence Connected { get; } = new("connected"); - /// Headless OAuth flow where a confidential client authenticates directly with a client secret. - public static McpOauthLoginGrantType ClientCredentials { get; } = new("client_credentials"); + /// 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 ==(McpOauthLoginGrantType left, McpOauthLoginGrantType 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 !=(McpOauthLoginGrantType left, McpOauthLoginGrantType 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 McpOauthLoginGrantType other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientOwnerPresence other && Equals(other); /// - public bool Equals(McpOauthLoginGrantType 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); @@ -33987,65 +36170,74 @@ 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 TaskClientOwnerPresence 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, TaskClientOwnerPresence value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOauthLoginGrantType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerPresence)); } } } -/// Why a passive MCP OAuth probe determined authentication is needed. +/// Lifecycle status of a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpOauthProbeNeedsAuthReason : 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 McpOauthProbeNeedsAuthReason(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; - /// No token was sent and the server requires authentication. - public static McpOauthProbeNeedsAuthReason Initial { get; } = new("initial"); + /// The external owner is actively working. + public static TaskClientStatus Running { get; } = new("running"); - /// A cached token was sent and rejected. - public static McpOauthProbeNeedsAuthReason Refresh { get; } = new("refresh"); + /// The external owner is connected but waiting. + public static TaskClientStatus Idle { get; } = new("idle"); - /// The server returned a 403 insufficient_scope challenge, indicating additional scopes or audience are needed. - public static McpOauthProbeNeedsAuthReason Upscope { get; } = new("upscope"); + /// 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 ==(McpOauthProbeNeedsAuthReason left, McpOauthProbeNeedsAuthReason 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 !=(McpOauthProbeNeedsAuthReason left, McpOauthProbeNeedsAuthReason 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 McpOauthProbeNeedsAuthReason other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientStatus other && Equals(other); /// - public bool Equals(McpOauthProbeNeedsAuthReason 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); @@ -34053,65 +36245,62 @@ 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 TaskClientStatus 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, TaskClientStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOauthProbeNeedsAuthReason)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientStatus)); } } } -/// Allowed values for the `McpAppsSetHostContextDetailsAvailableDisplayMode` enumeration. +/// 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 McpAppsSetHostContextDetailsAvailableDisplayMode : 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 McpAppsSetHostContextDetailsAvailableDisplayMode(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 McpAppsSetHostContextDetailsAvailableDisplayMode Inline { get; } = new("inline"); - - /// Rendered as a fullscreen overlay. - public static McpAppsSetHostContextDetailsAvailableDisplayMode 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 McpAppsSetHostContextDetailsAvailableDisplayMode 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 ==(McpAppsSetHostContextDetailsAvailableDisplayMode left, McpAppsSetHostContextDetailsAvailableDisplayMode 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 !=(McpAppsSetHostContextDetailsAvailableDisplayMode left, McpAppsSetHostContextDetailsAvailableDisplayMode 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 McpAppsSetHostContextDetailsAvailableDisplayMode other && Equals(other); + public override bool Equals(object? obj) => obj is TaskShellInfoAttachmentMode other && Equals(other); /// - public bool Equals(McpAppsSetHostContextDetailsAvailableDisplayMode 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); @@ -34119,65 +36308,59 @@ 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 TaskShellInfoAttachmentMode 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, TaskShellInfoAttachmentMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsAvailableDisplayMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskShellInfoAttachmentMode)); } } } -/// Current display mode (SEP-1865). +/// Discriminator for a client-owned task. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsSetHostContextDetailsDisplayMode : 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 McpAppsSetHostContextDetailsDisplayMode(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; - /// 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"); - - /// Rendered as a picture-in-picture floating panel. - public static McpAppsSetHostContextDetailsDisplayMode Pip { get; } = new("pip"); + /// Gets the client value. + public static TaskClientType Client { get; } = new("client"); - /// 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 ==(TaskClientType left, TaskClientType 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 !=(TaskClientType left, TaskClientType right) => !(left == right); /// - public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsDisplayMode other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientType other && Equals(other); /// - public bool Equals(McpAppsSetHostContextDetailsDisplayMode 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); @@ -34185,65 +36368,62 @@ 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 TaskClientType 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, TaskClientType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsDisplayMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientType)); } } } -/// Platform type for responsive design. +/// 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 McpAppsSetHostContextDetailsPlatform : 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 McpAppsSetHostContextDetailsPlatform(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; - /// 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"); + /// The external owner is actively working. + public static TaskClientActiveStatus Running { get; } = new("running"); - /// Host runs on a mobile device. - public static McpAppsSetHostContextDetailsPlatform Mobile { get; } = new("mobile"); + /// 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 ==(McpAppsSetHostContextDetailsPlatform left, McpAppsSetHostContextDetailsPlatform 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 !=(McpAppsSetHostContextDetailsPlatform left, McpAppsSetHostContextDetailsPlatform 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 McpAppsSetHostContextDetailsPlatform other && Equals(other); + public override bool Equals(object? obj) => obj is TaskClientActiveStatus other && Equals(other); /// - public bool Equals(McpAppsSetHostContextDetailsPlatform 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); @@ -34251,62 +36431,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 TaskClientActiveStatus 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, TaskClientActiveStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsPlatform)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientActiveStatus)); } } } -/// UI theme preference per SEP-1865. +/// Consumer allowed to call an MCP tool. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsSetHostContextDetailsTheme : 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 McpAppsSetHostContextDetailsTheme(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; - /// Light UI theme. - public static McpAppsSetHostContextDetailsTheme Light { get; } = new("light"); + /// The model may call the tool. + public static McpToolUiVisibility Model { get; } = new("model"); - /// Dark UI theme. - public static McpAppsSetHostContextDetailsTheme Dark { get; } = new("dark"); + /// 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 ==(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme 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 !=(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme 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 McpAppsSetHostContextDetailsTheme other && Equals(other); + public override bool Equals(object? obj) => obj is McpToolUiVisibility other && Equals(other); /// - public bool Equals(McpAppsSetHostContextDetailsTheme 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); @@ -34314,65 +36494,65 @@ 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 McpToolUiVisibility 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, McpToolUiVisibility value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsTheme)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpToolUiVisibility)); } } } -/// Allowed values for the `McpAppsHostContextDetailsAvailableDisplayMode` enumeration. +/// 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 McpAppsHostContextDetailsAvailableDisplayMode : 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 McpAppsHostContextDetailsAvailableDisplayMode(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 McpAppsHostContextDetailsAvailableDisplayMode 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 McpAppsHostContextDetailsAvailableDisplayMode 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 McpAppsHostContextDetailsAvailableDisplayMode 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 ==(McpAppsHostContextDetailsAvailableDisplayMode left, McpAppsHostContextDetailsAvailableDisplayMode 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 !=(McpAppsHostContextDetailsAvailableDisplayMode left, McpAppsHostContextDetailsAvailableDisplayMode 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 McpAppsHostContextDetailsAvailableDisplayMode other && Equals(other); + public override bool Equals(object? obj) => obj is McpSamplingExecutionAction other && Equals(other); /// - public bool Equals(McpAppsHostContextDetailsAvailableDisplayMode 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); @@ -34380,65 +36560,62 @@ 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 McpSamplingExecutionAction 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, McpSamplingExecutionAction value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsAvailableDisplayMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpSamplingExecutionAction)); } } } -/// Current display mode (SEP-1865). +/// 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 McpAppsHostContextDetailsDisplayMode : 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 McpAppsHostContextDetailsDisplayMode(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; - /// Rendered inline within the host conversation surface. - public static McpAppsHostContextDetailsDisplayMode Inline { get; } = new("inline"); - - /// Rendered as a fullscreen overlay. - public static McpAppsHostContextDetailsDisplayMode Fullscreen { get; } = new("fullscreen"); + /// Treat MCP server environment values as literal strings. + public static McpSetEnvValueModeDetails Direct { get; } = new("direct"); - /// Rendered as a picture-in-picture floating panel. - public static McpAppsHostContextDetailsDisplayMode Pip { get; } = new("pip"); + /// 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 ==(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode 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 !=(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode 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 McpAppsHostContextDetailsDisplayMode other && Equals(other); + public override bool Equals(object? obj) => obj is McpSetEnvValueModeDetails other && Equals(other); /// - public bool Equals(McpAppsHostContextDetailsDisplayMode 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); @@ -34446,65 +36623,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 McpSetEnvValueModeDetails 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, McpSetEnvValueModeDetails value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsDisplayMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpSetEnvValueModeDetails)); } } } -/// Platform type for responsive design. +/// Outcome of starting the original prepared owned login. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsHostContextDetailsPlatform : 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 McpAppsHostContextDetailsPlatform(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; - /// 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"); + /// The original requester may open the returned authorisation URL. + public static McpOwnedOauthLoginStatus AwaitingBrowser { get; } = new("awaiting-browser"); - /// Host runs on a mobile device. - public static McpAppsHostContextDetailsPlatform Mobile { get; } = new("mobile"); + /// 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 ==(McpAppsHostContextDetailsPlatform left, McpAppsHostContextDetailsPlatform 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 !=(McpAppsHostContextDetailsPlatform left, McpAppsHostContextDetailsPlatform 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 McpAppsHostContextDetailsPlatform other && Equals(other); + public override bool Equals(object? obj) => obj is McpOwnedOauthLoginStatus other && Equals(other); /// - public bool Equals(McpAppsHostContextDetailsPlatform 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); @@ -34512,62 +36686,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 McpOwnedOauthLoginStatus 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, McpOwnedOauthLoginStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsPlatform)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOwnedOauthLoginStatus)); } } } -/// UI theme preference per SEP-1865. +/// OAuth grant type override for this login. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpAppsHostContextDetailsTheme : 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 McpAppsHostContextDetailsTheme(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; - /// Light UI theme. - public static McpAppsHostContextDetailsTheme Light { get; } = new("light"); + /// Interactive browser-based OAuth flow using an authorization code, typically with PKCE. + public static McpOauthLoginGrantType AuthorizationCode { get; } = new("authorization_code"); - /// Dark UI theme. - public static McpAppsHostContextDetailsTheme Dark { get; } = new("dark"); + /// 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 ==(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme 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 !=(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme 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 McpAppsHostContextDetailsTheme other && Equals(other); + public override bool Equals(object? obj) => obj is McpOauthLoginGrantType other && Equals(other); /// - public bool Equals(McpAppsHostContextDetailsTheme 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); @@ -34575,74 +36749,65 @@ 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 McpOauthLoginGrantType 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, McpOauthLoginGrantType value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsTheme)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOauthLoginGrantType)); } } } -/// Session-scoped diagnostic threshold. Capture is disabled by default and is never persisted with the session. +/// 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 DiagnosticLogLevel : 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 DiagnosticLogLevel(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; - /// 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"); + /// No token was sent and the server requires authentication. + public static McpOauthProbeNeedsAuthReason Initial { get; } = new("initial"); - /// Capture protocol frames and launch diagnostics. - public static DiagnosticLogLevel Debug { get; } = new("debug"); + /// A cached token was sent and rejected. + public static McpOauthProbeNeedsAuthReason Refresh { get; } = new("refresh"); - /// Capture HTTP metadata in addition to debug diagnostics. - public static DiagnosticLogLevel Trace { get; } = new("trace"); + /// 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 ==(DiagnosticLogLevel left, DiagnosticLogLevel 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 !=(DiagnosticLogLevel left, DiagnosticLogLevel 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 DiagnosticLogLevel other && Equals(other); + public override bool Equals(object? obj) => obj is McpOauthProbeNeedsAuthReason other && Equals(other); /// - public bool Equals(DiagnosticLogLevel 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); @@ -34650,62 +36815,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 McpOauthProbeNeedsAuthReason 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, McpOauthProbeNeedsAuthReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticLogLevel)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOauthProbeNeedsAuthReason)); } } } -/// Whether the supplied diagnostic cursor remained within the retained buffer window. +/// Allowed values for the `McpAppsSetHostContextDetailsAvailableDisplayMode` enumeration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiagnosticCursorStatus : 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 DiagnosticCursorStatus(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; - /// The cursor is valid for the current retained window. - public static DiagnosticCursorStatus Ok { get; } = new("ok"); + /// Rendered inline within the host conversation surface. + public static McpAppsSetHostContextDetailsAvailableDisplayMode Inline { get; } = new("inline"); - /// The cursor no longer addresses retained records; reading resumes at the oldest retained record. - public static DiagnosticCursorStatus Expired { get; } = new("expired"); + /// 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 ==(DiagnosticCursorStatus left, DiagnosticCursorStatus 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 !=(DiagnosticCursorStatus left, DiagnosticCursorStatus 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 DiagnosticCursorStatus other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsAvailableDisplayMode other && Equals(other); /// - public bool Equals(DiagnosticCursorStatus 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); @@ -34713,62 +36881,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 McpAppsSetHostContextDetailsAvailableDisplayMode 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, McpAppsSetHostContextDetailsAvailableDisplayMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticCursorStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsAvailableDisplayMode)); } } } -/// Direction of an observed MCP protocol frame. +/// Current display mode (SEP-1865). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpDiagnosticDirection : 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 McpDiagnosticDirection(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; - /// 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 McpAppsSetHostContextDetailsDisplayMode 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 McpAppsSetHostContextDetailsDisplayMode 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 McpAppsSetHostContextDetailsDisplayMode 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 ==(McpAppsSetHostContextDetailsDisplayMode left, McpAppsSetHostContextDetailsDisplayMode right) => left.Equals(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 McpDiagnosticDirection other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsDisplayMode other && Equals(other); /// - public bool Equals(McpDiagnosticDirection 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); @@ -34776,68 +36947,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 McpAppsSetHostContextDetailsDisplayMode 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, McpAppsSetHostContextDetailsDisplayMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpDiagnosticDirection)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsDisplayMode)); } } } -/// Category for an MCP diagnostic record. +/// Platform type for responsive design. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct McpDiagnosticKind : 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 McpDiagnosticKind(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; - /// Connection lifecycle transition or failure. - public static McpDiagnosticKind Lifecycle { get; } = new("lifecycle"); - - /// JSON-RPC protocol frame. - public static McpDiagnosticKind Protocol { get; } = new("protocol"); + /// Host runs in a web browser. + public static McpAppsSetHostContextDetailsPlatform Web { get; } = new("web"); - /// HTTP request or response metadata. - public static McpDiagnosticKind Http { get; } = new("http"); + /// Host runs as a desktop application. + public static McpAppsSetHostContextDetailsPlatform Desktop { get; } = new("desktop"); - /// Local MCP server standard-error output. - public static McpDiagnosticKind Stderr { get; } = new("stderr"); + /// 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 ==(McpDiagnosticKind left, McpDiagnosticKind 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 !=(McpDiagnosticKind left, McpDiagnosticKind 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 McpDiagnosticKind other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsPlatform other && Equals(other); /// - public bool Equals(McpDiagnosticKind 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); @@ -34845,71 +37013,62 @@ 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 McpAppsSetHostContextDetailsPlatform 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, McpAppsSetHostContextDetailsPlatform value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpDiagnosticKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsPlatform)); } } } -/// Severity of an emitted diagnostic record. +/// UI theme preference per SEP-1865. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiagnosticSeverity : 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 DiagnosticSeverity(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; - /// 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"); - - /// Protocol-frame or launch diagnostic. - public static DiagnosticSeverity Debug { get; } = new("debug"); + /// Light UI theme. + public static McpAppsSetHostContextDetailsTheme Light { get; } = new("light"); - /// HTTP metadata diagnostic. - public static DiagnosticSeverity Trace { get; } = new("trace"); + /// Dark UI theme. + public static McpAppsSetHostContextDetailsTheme Dark { get; } = new("dark"); - /// 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 ==(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme 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 !=(McpAppsSetHostContextDetailsTheme left, McpAppsSetHostContextDetailsTheme right) => !(left == right); /// - public override bool Equals(object? obj) => obj is DiagnosticSeverity other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsSetHostContextDetailsTheme other && Equals(other); /// - public bool Equals(DiagnosticSeverity 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); @@ -34917,59 +37076,65 @@ 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 McpAppsSetHostContextDetailsTheme 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, McpAppsSetHostContextDetailsTheme value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticSeverity)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsSetHostContextDetailsTheme)); } } } -/// A supported diagnostic source. +/// Allowed values for the `McpAppsHostContextDetailsAvailableDisplayMode` enumeration. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct DiagnosticSource : 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 DiagnosticSource(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; - /// Gets the mcp value. - public static DiagnosticSource Mcp { get; } = new("mcp"); + /// Rendered inline within the host conversation surface. + public static McpAppsHostContextDetailsAvailableDisplayMode Inline { get; } = new("inline"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(DiagnosticSource left, DiagnosticSource right) => left.Equals(right); + /// Rendered as a fullscreen overlay. + public static McpAppsHostContextDetailsAvailableDisplayMode Fullscreen { get; } = new("fullscreen"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(DiagnosticSource left, DiagnosticSource right) => !(left == right); + /// 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 ==(McpAppsHostContextDetailsAvailableDisplayMode left, McpAppsHostContextDetailsAvailableDisplayMode right) => left.Equals(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 DiagnosticSource other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsAvailableDisplayMode other && Equals(other); /// - public bool Equals(DiagnosticSource 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); @@ -34977,65 +37142,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 McpAppsHostContextDetailsAvailableDisplayMode 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, McpAppsHostContextDetailsAvailableDisplayMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticSource)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsAvailableDisplayMode)); } } } -/// Availability of the EXPERIMENTAL session connector API. +/// Current display mode (SEP-1865). [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectorAvailability : 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 ConnectorAvailability(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; - /// 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 McpAppsHostContextDetailsDisplayMode 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 McpAppsHostContextDetailsDisplayMode 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 McpAppsHostContextDetailsDisplayMode 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 ==(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode 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 !=(McpAppsHostContextDetailsDisplayMode left, McpAppsHostContextDetailsDisplayMode right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ConnectorAvailability other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsDisplayMode other && Equals(other); /// - public bool Equals(ConnectorAvailability 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); @@ -35043,59 +37208,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 McpAppsHostContextDetailsDisplayMode 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, McpAppsHostContextDetailsDisplayMode value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorAvailability)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsDisplayMode)); } } } -/// Stable OAuth scope whose absence prevents Connector management. +/// Platform type for responsive design. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectorAuthorizationScope : 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 ConnectorAuthorizationScope(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; - /// Allows Copilot to manage Connector connections and use their tools. - public static ConnectorAuthorizationScope WritePluginGatewayConnections { get; } = new("write_plugin_gateway_connections"); + /// Host runs in a web browser. + public static McpAppsHostContextDetailsPlatform Web { get; } = new("web"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ConnectorAuthorizationScope left, ConnectorAuthorizationScope right) => left.Equals(right); + /// Host runs as a desktop application. + public static McpAppsHostContextDetailsPlatform Desktop { get; } = new("desktop"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ConnectorAuthorizationScope left, ConnectorAuthorizationScope right) => !(left == right); + /// 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 ==(McpAppsHostContextDetailsPlatform left, McpAppsHostContextDetailsPlatform right) => left.Equals(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 ConnectorAuthorizationScope other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsPlatform other && Equals(other); /// - public bool Equals(ConnectorAuthorizationScope 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); @@ -35103,71 +37274,62 @@ 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 McpAppsHostContextDetailsPlatform 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, McpAppsHostContextDetailsPlatform value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorAuthorizationScope)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsPlatform)); } } } -/// Authoritative service connection state for one Connector. +/// UI theme preference per SEP-1865. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectorCatalogStatus : 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 ConnectorCatalogStatus(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 Connector is available but not connected. - public static ConnectorCatalogStatus NotConnected { get; } = new("not_connected"); - - /// The Connector service is still completing connection or consent. - public static ConnectorCatalogStatus Pending { get; } = new("pending"); - - /// The Connector is connected and may contribute MCP servers. - public static ConnectorCatalogStatus Connected { get; } = new("connected"); - - /// The Connector service reports an unusable connection. - public static ConnectorCatalogStatus Error { get; } = new("error"); + /// Light UI theme. + public static McpAppsHostContextDetailsTheme Light { get; } = new("light"); - /// The service returned a future or unrecognized state. - public static ConnectorCatalogStatus Unknown { get; } = new("unknown"); + /// Dark UI theme. + public static McpAppsHostContextDetailsTheme Dark { get; } = new("dark"); - /// 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 ==(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme 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 !=(McpAppsHostContextDetailsTheme left, McpAppsHostContextDetailsTheme right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ConnectorCatalogStatus other && Equals(other); + public override bool Equals(object? obj) => obj is McpAppsHostContextDetailsTheme other && Equals(other); /// - public bool Equals(ConnectorCatalogStatus 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); @@ -35175,77 +37337,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 McpAppsHostContextDetailsTheme 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, McpAppsHostContextDetailsTheme value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorCatalogStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpAppsHostContextDetailsTheme)); } } } -/// Live MCP status of one Connector-owned runtime server. +/// The sender role of an MCP prompt message. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ConnectorMcpStatus : 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 ConnectorMcpStatus(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; - /// 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"); + /// A message from the user. + public static McpPromptRole User { get; } = new("user"); - /// The Connector currently has no live server configuration. - public static ConnectorMcpStatus NotConfigured { get; } = new("not_configured"); + /// 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 ==(ConnectorMcpStatus left, ConnectorMcpStatus 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 !=(ConnectorMcpStatus left, ConnectorMcpStatus 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 ConnectorMcpStatus other && Equals(other); + public override bool Equals(object? obj) => obj is McpPromptRole other && Equals(other); /// - public bool Equals(ConnectorMcpStatus 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); @@ -35253,62 +37400,74 @@ 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 McpPromptRole 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, McpPromptRole value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorMcpStatus)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpPromptRole)); } } } -/// Transport to be used for provider requests. +/// 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 ProviderEndpointTransport : 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 ProviderEndpointTransport(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; - /// HTTP request/streaming transport. - public static ProviderEndpointTransport Http { get; } = new("http"); + /// Disable capture and clear retained diagnostics. + public static DiagnosticLogLevel Off { get; } = new("off"); - /// WebSocket transport. - public static ProviderEndpointTransport Websockets { get; } = new("websockets"); + /// Capture failures only. + public static DiagnosticLogLevel Error { get; } = new("error"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderEndpointTransport left, ProviderEndpointTransport right) => left.Equals(right); + /// Capture failures, warnings, and stderr. + public static DiagnosticLogLevel Warning { get; } = new("warning"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderEndpointTransport left, ProviderEndpointTransport right) => !(left == right); + /// Capture lifecycle diagnostics. + public static DiagnosticLogLevel Info { get; } = new("info"); + + /// 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 ProviderEndpointTransport other && Equals(other); + public override bool Equals(object? obj) => obj is DiagnosticLogLevel other && Equals(other); /// - public bool Equals(ProviderEndpointTransport 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); @@ -35316,65 +37475,62 @@ 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 DiagnosticLogLevel 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, DiagnosticLogLevel value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointTransport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticLogLevel)); } } } -/// Provider family. Matches the `type` field of a BYOK provider config. +/// 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 ProviderEndpointType : 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 ProviderEndpointType(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; - /// OpenAI-compatible endpoint (use the OpenAI client library). - public static ProviderEndpointType Openai { get; } = new("openai"); - - /// Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). - public static ProviderEndpointType Azure { get; } = new("azure"); + /// The cursor is valid for the current retained window. + public static DiagnosticCursorStatus Ok { get; } = new("ok"); - /// Anthropic endpoint (use the Anthropic client library). - public static ProviderEndpointType Anthropic { get; } = new("anthropic"); + /// 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 ==(ProviderEndpointType left, ProviderEndpointType 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 !=(ProviderEndpointType left, ProviderEndpointType 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 ProviderEndpointType other && Equals(other); + public override bool Equals(object? obj) => obj is DiagnosticCursorStatus other && Equals(other); /// - public bool Equals(ProviderEndpointType 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); @@ -35382,62 +37538,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 DiagnosticCursorStatus 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, DiagnosticCursorStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticCursorStatus)); } } } -/// Wire API to be used, when required for the provider type. +/// Direction of an observed MCP protocol frame. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderEndpointWireApi : 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 ProviderEndpointWireApi(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; - /// Classic chat-completions request shape. - public static ProviderEndpointWireApi Completions { get; } = new("completions"); + /// Frame emitted by the Copilot MCP client. + public static McpDiagnosticDirection ClientToServer { get; } = new("client-to-server"); - /// Newer responses request shape. - public static ProviderEndpointWireApi Responses { get; } = new("responses"); + /// Frame received from the MCP server. + public static McpDiagnosticDirection ServerToClient { get; } = new("server-to-client"); - /// 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 ==(McpDiagnosticDirection left, McpDiagnosticDirection 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 !=(McpDiagnosticDirection left, McpDiagnosticDirection right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProviderEndpointWireApi other && Equals(other); + public override bool Equals(object? obj) => obj is McpDiagnosticDirection other && Equals(other); /// - public bool Equals(ProviderEndpointWireApi 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); @@ -35445,59 +37601,68 @@ 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 McpDiagnosticDirection 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, McpDiagnosticDirection value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderEndpointWireApi)); + 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 ProtocolAppendMode : 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 ProtocolAppendMode(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 append value. - public static ProtocolAppendMode Append { get; } = new("append"); + /// 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 ==(ProtocolAppendMode left, ProtocolAppendMode 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 !=(ProtocolAppendMode left, ProtocolAppendMode 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 ProtocolAppendMode other && Equals(other); + public override bool Equals(object? obj) => obj is McpDiagnosticKind other && Equals(other); /// - public bool Equals(ProtocolAppendMode 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); @@ -35505,59 +37670,71 @@ 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 McpDiagnosticKind 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, McpDiagnosticKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolAppendMode)); + 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 ProtocolReplaceMode : 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 ProtocolReplaceMode(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 replace value. - public static ProtocolReplaceMode Replace { get; } = new("replace"); + /// 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 ==(ProtocolReplaceMode left, ProtocolReplaceMode 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 !=(ProtocolReplaceMode left, ProtocolReplaceMode 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 ProtocolReplaceMode other && Equals(other); + public override bool Equals(object? obj) => obj is DiagnosticSeverity other && Equals(other); /// - public bool Equals(ProtocolReplaceMode 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); @@ -35565,59 +37742,59 @@ 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 DiagnosticSeverity 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, DiagnosticSeverity value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolReplaceMode)); + 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 ProtocolCustomizeMode : 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 ProtocolCustomizeMode(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; - /// Gets the customize value. - public static ProtocolCustomizeMode Customize { get; } = new("customize"); + /// Gets the mcp value. + public static DiagnosticSource Mcp { get; } = new("mcp"); - /// 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 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 !=(ProtocolCustomizeMode left, ProtocolCustomizeMode 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 ProtocolCustomizeMode other && Equals(other); + public override bool Equals(object? obj) => obj is DiagnosticSource other && Equals(other); /// - public bool Equals(ProtocolCustomizeMode 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); @@ -35625,68 +37802,65 @@ 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 DiagnosticSource 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, DiagnosticSource value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolCustomizeMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(DiagnosticSource)); } } } -/// Defines the allowed values. +/// Availability of the EXPERIMENTAL session connector API. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProtocolStaticSectionAction : 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 ProtocolStaticSectionAction(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; - /// Replace the section content. - public static ProtocolStaticSectionAction Replace { get; } = new("replace"); - - /// Remove the section content. - public static ProtocolStaticSectionAction Remove { get; } = new("remove"); + /// The resolved Connector feature is enabled and Connector requests are permitted. + public static ConnectorAvailability Enabled { get; } = new("enabled"); - /// Append content to the section. - public static ProtocolStaticSectionAction Append { get; } = new("append"); + /// The resolved Connector feature is off. No Connector service request is made while disabled. + public static ConnectorAvailability Disabled { get; } = new("disabled"); - /// Prepend content to the section. - public static ProtocolStaticSectionAction Prepend { get; } = new("prepend"); + /// 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 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 ==(ConnectorAvailability left, ConnectorAvailability 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 !=(ConnectorAvailability left, ConnectorAvailability right) => !(left == right); /// - public override bool Equals(object? obj) => obj is ProtocolStaticSectionAction other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectorAvailability other && Equals(other); /// - public bool Equals(ProtocolStaticSectionAction 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); @@ -35694,62 +37868,59 @@ 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 ConnectorAvailability 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, ConnectorAvailability value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProtocolStaticSectionAction)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorAvailability)); } } } -/// Provider transport. Defaults to "http". +/// Stable OAuth scope whose absence prevents Connector management. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderConfigTransport : 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 ProviderConfigTransport(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; - /// HTTP request/streaming transport. - public static ProviderConfigTransport Http { get; } = new("http"); - - /// WebSocket transport. - public static ProviderConfigTransport Websockets { get; } = new("websockets"); + /// 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 ==(ProviderConfigTransport left, ProviderConfigTransport 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 !=(ProviderConfigTransport left, ProviderConfigTransport 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 ProviderConfigTransport other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectorAuthorizationScope other && Equals(other); /// - public bool Equals(ProviderConfigTransport 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); @@ -35757,65 +37928,71 @@ 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 ConnectorAuthorizationScope 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, ConnectorAuthorizationScope value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigTransport)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorAuthorizationScope)); } } } -/// Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. +/// Authoritative service connection state for one Connector. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct ProviderConfigType : 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 ProviderConfigType(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; - /// Generic OpenAI-compatible API. - public static ProviderConfigType Openai { get; } = new("openai"); + /// The Connector is available but not connected. + public static ConnectorCatalogStatus NotConnected { get; } = new("not_connected"); - /// Azure OpenAI Service endpoint. - public static ProviderConfigType Azure { get; } = new("azure"); + /// The Connector service is still completing connection or consent. + public static ConnectorCatalogStatus Pending { get; } = new("pending"); - /// Anthropic API endpoint. - public static ProviderConfigType Anthropic { get; } = new("anthropic"); + /// The Connector is connected and may contribute MCP servers. + public static ConnectorCatalogStatus Connected { get; } = new("connected"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(ProviderConfigType left, ProviderConfigType right) => left.Equals(right); + /// The Connector service reports an unusable connection. + public static ConnectorCatalogStatus Error { get; } = new("error"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(ProviderConfigType left, ProviderConfigType right) => !(left == right); + /// 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 ProviderConfigType other && Equals(other); + public override bool Equals(object? obj) => obj is ConnectorCatalogStatus other && Equals(other); /// - public bool Equals(ProviderConfigType 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); @@ -35823,62 +38000,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 ConnectorCatalogStatus 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, ConnectorCatalogStatus value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ProviderConfigType)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ConnectorCatalogStatus)); } } } -/// 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); @@ -35886,20 +38078,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)); } } } @@ -39934,6 +42126,12 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell await CopilotClient.InvokeRpcAsync(_rpc, "registerExtensionLaunchProvider", [], cancellationToken); } + /// Environments APIs. + public ServerEnvironmentsApi Environments => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + /// Host APIs. public ServerHostApi Host => field ?? @@ -40065,12 +42263,53 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell field ?? Interlocked.CompareExchange(ref field, new(_rpc), null) ?? field; +} - /// Accounts APIs. - public ServerAccountsApi Accounts => - field ?? - Interlocked.CompareExchange(ref field, new(_rpc), null) ?? - field; +/// Provides server-scoped Environments APIs. +[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] +public sealed class ServerEnvironmentsApi +{ + private readonly JsonRpc _rpc; + + internal ServerEnvironmentsApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Lists GitHub Mission Control environments visible to the authenticated identity. Does not require a running host and excludes host relay credentials. + /// Restrict discovery to this compute kind. + /// Operational status, such as online, offline, degraded, waking, or draining. + /// The to monitor for cancellation requests. The default is . + /// Environments visible to the authenticated caller and matching the supplied filters. + public async Task ListAsync(EnvironmentKind? kind = null, string? status = null, CancellationToken cancellationToken = default) + { + var request = new EnvironmentsListRequest { Kind = kind, Status = status }; + return await CopilotClient.InvokeRpcAsync(_rpc, "environments.list", [request], cancellationToken); + } + + /// Gets safe discovery information for a GitHub Mission Control environment without requiring a running host. + /// Identifier assigned by Mission Control. + /// The to monitor for cancellation requests. The default is . + /// Safe discovery information for the requested environment. + public async Task GetAsync(string environmentId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(environmentId); + + var request = new EnvironmentsGetRequest { EnvironmentId = environmentId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "environments.get", [request], cancellationToken); + } + + /// Deletes a user-managed GitHub Mission Control environment. GitHub-managed environments cannot be deleted. Does not stop a running host, which may register again. + /// User-managed environment to delete. GitHub-managed environments cannot be deleted. + /// The to monitor for cancellation requests. The default is . + /// Acknowledgement that the requested environment was deleted. + public async Task DeleteAsync(string environmentId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(environmentId); + + var request = new EnvironmentsDeleteRequest { EnvironmentId = environmentId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "environments.delete", [request], cancellationToken); + } } /// Provides server-scoped Host APIs. @@ -40125,21 +42364,19 @@ internal async Task ReleaseSessionAsync(string handoff return await CopilotClient.InvokeRpcAsync(_rpc, "host.releaseSession", [request], cancellationToken); } - /// Starts a connection-owned local AHP listener as a supervised SDK participant. + /// Starts a connection-owned AHP host with explicit localServer and/or githubEnvironment transports as a supervised SDK participant. /// Caller-generated UUID identifying this connection-owned listener. - /// Listener hostname. Defaults to 127.0.0.1; explicit non-loopback binds are allowed. - /// Listener port. Omitted or zero requests an OS-allocated port. - /// Nonempty connection token. Generated randomly when required and omitted. - /// Require token authentication (default true). Cannot be false with a token. + /// Enables a local WebSocket listener. + /// Registers a GitHub Mission Control environment and enables its relay transport. /// Ask the owning SDK application to materialize AHP sessions. /// Ask the owning application to resume its durable AHP sessions. /// The to monitor for cancellation requests. The default is . /// Listener readiness, returned only after binding and the supervised participant's SDK handshake. - public async Task StartAsync(string hostId, string? hostname = null, int? port = null, string? token = null, bool? requireConnectionToken = null, bool? sessionFactory = null, bool? resumeFactory = null, CancellationToken cancellationToken = default) + public async Task StartAsync(string hostId, HostLocalServerOptions? localServer = null, HostGitHubEnvironmentOptions? githubEnvironment = null, bool? sessionFactory = null, bool? resumeFactory = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(hostId); - var request = new HostStartRequest { HostId = hostId, Hostname = hostname, Port = port, Token = token, RequireConnectionToken = requireConnectionToken, SessionFactory = sessionFactory, ResumeFactory = resumeFactory }; + var request = new HostStartRequest { HostId = hostId, LocalServer = localServer, GitHubEnvironment = githubEnvironment, SessionFactory = sessionFactory, ResumeFactory = resumeFactory }; return await CopilotClient.InvokeRpcAsync(_rpc, "host.start", [request], cancellationToken); } @@ -40155,6 +42392,14 @@ public async Task DisposeAsync(string hostId, CancellationTok return await CopilotClient.InvokeRpcAsync(_rpc, "host.dispose", [request], cancellationToken); } + /// Resolves current authenticated credentials and remote-control policy only for the runtime-owned Mission Control hosting participant. + /// The to monitor for cancellation requests. The default is . + /// Private credentials delivered only to a runtime-owned Mission Control hosting participant. + internal async Task GetEnvironmentCredentialsAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "host.getEnvironmentCredentials", [], cancellationToken); + } + /// Returns listener settings only to the supervised hosting participant over its SDK connection. /// The to monitor for cancellation requests. The default is . /// Normalized listener settings delivered only to the supervised hosting participant. @@ -40166,13 +42411,12 @@ internal async Task GetConfigurationAsync(Cancellati /// Reports a supervised hosting participant's bound AHP endpoint after its SDK handshake. /// Actual bound WebSocket URL. /// Configured secret token, absent when authentication is disabled. + /// Registered environment ID, reported only once the relay transport is connected. /// The to monitor for cancellation requests. The default is . /// Empty acknowledgement for a completed host lifecycle operation. - internal async Task ReadyAsync(string address, string? token = null, CancellationToken cancellationToken = default) + internal async Task ReadyAsync(string? address = null, string? token = null, string? environmentId = null, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(address); - - var request = new HostReadyRequest { Address = address, Token = token }; + var request = new HostReadyRequest { Address = address, Token = token, EnvironmentId = environmentId }; return await CopilotClient.InvokeRpcAsync(_rpc, "host.ready", [request], cancellationToken); } } @@ -40249,6 +42493,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. @@ -41339,7 +43649,7 @@ internal ServerManagedSettingsApi(JsonRpc rpc) _rpc = rpc; } - /// Discovers device-managed settings from production MDM and managed-file sources, validates them against the runtime-owned managed-settings schema, and returns the canonical JSON without requiring a session. + /// Discovers device-managed settings from production MDM and managed-file sources, validates them against the runtime-owned managed-settings schema, and returns the canonical JSON without requiring a session. `managedSettings.resolve` returns the same device settings together with the account's server policy. /// The to monitor for cancellation requests. The default is . /// Validated device-managed settings discovered before a session exists. public async Task ReadAsync(CancellationToken cancellationToken = default) @@ -41347,12 +43657,57 @@ public async Task ReadAsync(CancellationToken cancell return await CopilotClient.InvokeRpcAsync(_rpc, "managedSettings.read", [], cancellationToken); } - /// Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `<cacheHome>/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing "sync account policy" should start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed. + /// Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `<cacheHome>/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing "sync account policy" should call `managedSettings.resolve` or start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed. /// The to monitor for cancellation requests. The default is . public async Task ClearCacheAsync(CancellationToken cancellationToken = default) { await CopilotClient.InvokeRpcAsync(_rpc, "managedSettings.clearCache", [], cancellationToken); } + + /// Resolves the effective enterprise managed settings without a session, from the device channel and, when an account is available, the account's server policy through the same per-account cache sessions use. A cached server policy less than an hour old is used without a fetch; otherwise the policy is fetched, and when the fetch fails a cached policy up to 24 hours old is used instead, unless `forceRemoteSettingsRefresh` requires a live fetch. With no account requested or signed in, it reports device policy only; signing out removes the account's cached policy. It can fetch server policy over the network when the cache is stale, so call it off latency-critical paths such as startup rather than before listing models. The policy helper is not run. `layers` lists each channel's document before merging, and `values` and `meta` carry typed effective values and their lock state for the keys typed so far. + /// Opaque account identifier returned by `account.getAllUsers`. When omitted, the current account is used, or device policy only when no account is signed in. + /// GitHub token to resolve instead of the current account. The call fails when the token cannot be resolved. + /// Embedding client identity for server policy requests, as in session creation. Omit for the CLI identity. + /// The to monitor for cancellation requests. The default is . + /// Effective enterprise managed settings for an account, resolved without a session. + public async Task ResolveAsync(string? selectionId = null, string? gitHubToken = null, string? clientName = null, CancellationToken cancellationToken = default) + { + var request = new ManagedSettingsResolveRequest { SelectionId = selectionId, GitHubToken = gitHubToken, ClientName = clientName }; + return await CopilotClient.InvokeRpcAsync(_rpc, "managedSettings.resolve", [request], cancellationToken); + } + + /// Returns the managed-settings authoring JSON schema with descriptive `x-composition` annotations aligned with the shared settings-engine vocabulary. These annotations are not a complete runtime composition contract: model, effortLevel, and contextTier remain coupled. Use `managedSettings.compose` for the runtime's effective result. Performs no I/O. + /// The to monitor for cancellation requests. The default is . + /// The authoring JSON schema for managed settings recognized by this runtime. + public async Task SchemaAsync(CancellationToken cancellationToken = default) + { + return await CopilotClient.InvokeRpcAsync(_rpc, "managedSettings.schema", [], cancellationToken); + } + + /// Validates a candidate managed-settings document the way the runtime validates delivered policy, without applying it. Reports errors that would reject the document, warnings for content the runtime ignores, and the canonical document it would apply. Document text nested more than 64 levels deep is rejected. Performs no I/O. + /// The document to validate: a JSON object, or a string containing the document's JSON text. Preview documents are limited to 1 MiB and 64 levels of nesting, a stricter resource limit than delivered-policy parsing; violations are returned as diagnostics. + /// Channel the document is meant for (`device`, `server`, or `policyHelper`). Some keys are only honored in some channels; for example, a `policyHelper` registration is ignored in policy-helper output. When omitted, no channel-specific checks run. + /// The to monitor for cancellation requests. The default is . + /// Result of validating a managed-settings document. + public async Task ValidateAsync(object content, string? layer = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(content); + + var request = new ManagedSettingsValidateRequest { Content = CopilotClient.ToJsonElementForWire(content)!.Value, Layer = layer }; + return await CopilotClient.InvokeRpcAsync(_rpc, "managedSettings.validate", [request], cancellationToken); + } + + /// Merges candidate managed-settings documents for the device, server, and policy-helper channels into the effective settings the runtime would enforce on this host, using the same precedence and composition rules as live resolution, without applying them. Like live resolution, a server's advisory sandbox force-enable is declined on a host that cannot run the sandbox. Does not fetch policy or read policy files, but may perform blocking OS or subprocess probes for sandbox support. Preview documents are limited to 1 MiB and 64 levels of nesting. + /// One entry per channel. `source` must be `device`, `server`, or `policyHelper`, each at most once (checked at runtime); order does not matter, because channel precedence is fixed. To preview documents from resolve output, map recognized source strings to ManagedSettingsChannel and copy their settings; generated resolve and compose layer types are distinct. Omitted settings means this channel delivered no document. Supplied documents must be valid within the preview limits; warnings are returned in diagnostics. Compose does not reproduce source-failure state or retained enforcement floors from resolve. + /// The to monitor for cancellation requests. The default is . + /// The effective managed settings the runtime would enforce for the given documents, in the same shape `managedSettings.resolve` returns. + public async Task ComposeAsync(IList layers, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(layers); + + var request = new ManagedSettingsComposeRequest { Layers = layers }; + return await CopilotClient.InvokeRpcAsync(_rpc, "managedSettings.compose", [request], cancellationToken); + } } /// Provides server-scoped Runtime APIs. @@ -41873,38 +44228,6 @@ public async Task SpawnAsync(string cwd, string? agent } } -/// Provides server-scoped Accounts APIs. -[Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] -public sealed class ServerAccountsApi -{ - private readonly JsonRpc _rpc; - - internal ServerAccountsApi(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. - /// 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) - { - ArgumentNullException.ThrowIfNull(clientId); - ArgumentNullException.ThrowIfNull(tenantId); - ArgumentNullException.ThrowIfNull(redirectUri); - ArgumentNullException.ThrowIfNull(scopes); - - 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); - } -} - /// Provides typed session-scoped RPC methods. public sealed class SessionRpc { @@ -41917,6 +44240,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 ?? @@ -42334,6 +44663,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 @@ -43689,14 +46114,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); } } @@ -43711,15 +46137,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); } } @@ -44112,7 +46538,7 @@ 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. + /// 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) @@ -44123,6 +46549,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 . @@ -44416,6 +46853,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. @@ -44495,7 +46938,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) @@ -44503,10 +46946,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 . @@ -44740,6 +47197,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 @@ -44803,6 +47302,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. @@ -44911,10 +47421,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 . @@ -44953,7 +47476,7 @@ internal ManagedSettingsApi(CopilotSession session) /// Waits for the live session's in-flight managed-settings application, then returns the retained effective snapshot used by runtime enforcement and by `session.managed_settings_resolved`. It does not perform another account, device, or server resolution, and rejects when resolution has not produced a snapshot. /// The to monitor for cancellation requests. The default is . - /// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. + /// Effective enterprise managed settings and contributing channels. Session events report applied policy; sessionless resolve reports an account/device snapshot, and compose reports a non-applying preview of candidate documents. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively. Session-local SDK-client policy is included only in session results. Marked experimental while the managed-settings surface stabilizes. public async Task GetAsync(CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); @@ -45190,6 +47713,19 @@ public async Task SyncAsync(IList? prov var request = new ProviderSyncRequest { SessionId = _session.SessionId, Providers = providers, Models = models }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.provider.sync", [request], cancellationToken); } + + /// Withdraws named host-managed models from the session's BYOK registry, leaving every other entry untouched. The scoped counterpart to `provider.sync`: a snapshot can only describe entries the caller knows about, so using it to remove one model silently withdraws rows registered by another source, such as a plugin calling `provider.add` at runtime. Naming what to remove leaves unrelated entries alone. Selection ids that are not registered are ignored, so withdrawal is idempotent. A provider is removed only when one of the withdrawn models was the last entry referencing it; a provider that simply has no models, which is the normal state while its rows are supplied by catalog discovery, is left in place. + /// 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. + /// The to monitor for cancellation requests. The default is . + /// What the withdrawal actually removed from the registry. + public async Task WithdrawAsync(IList models, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(models); + _session.ThrowIfDisposed(); + + var request = new ProviderWithdrawRequest { SessionId = _session.SessionId, Models = models }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.provider.withdraw", [request], cancellationToken); + } } /// Provides session-scoped Options APIs. @@ -45696,6 +48232,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. @@ -45711,6 +48262,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. @@ -45769,6 +48335,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). @@ -46372,12 +48953,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); @@ -47195,11 +49776,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 . @@ -47336,12 +49927,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; @@ -47746,6 +50349,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")] @@ -47789,6 +50396,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.ModelCallFailureRequestFingerprint), TypeInfoPropertyName = "SessionEventsModelCallFailureRequestFingerprint")] [JsonSerializable(typeof(GitHub.Copilot.ModelCallFailureSource), TypeInfoPropertyName = "SessionEventsModelCallFailureSource")] [JsonSerializable(typeof(GitHub.Copilot.ModelCallFailureTransport), TypeInfoPropertyName = "SessionEventsModelCallFailureTransport")] +[JsonSerializable(typeof(GitHub.Copilot.ModelCallFinalResult), TypeInfoPropertyName = "SessionEventsModelCallFinalResult")] +[JsonSerializable(typeof(GitHub.Copilot.ModelCallFinalResultData), TypeInfoPropertyName = "SessionEventsModelCallFinalResultData")] +[JsonSerializable(typeof(GitHub.Copilot.ModelCallFinalResultEvent), TypeInfoPropertyName = "SessionEventsModelCallFinalResultEvent")] [JsonSerializable(typeof(GitHub.Copilot.ModelCallFinishedData), TypeInfoPropertyName = "SessionEventsModelCallFinishedData")] [JsonSerializable(typeof(GitHub.Copilot.ModelCallFinishedEvent), TypeInfoPropertyName = "SessionEventsModelCallFinishedEvent")] [JsonSerializable(typeof(GitHub.Copilot.ModelCallFinishedOutcome), TypeInfoPropertyName = "SessionEventsModelCallFinishedOutcome")] @@ -47978,6 +50588,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")] @@ -48005,6 +50617,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")] @@ -48073,7 +50688,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [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))] @@ -48160,7 +50777,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ConnectorContinueRequest))] [JsonSerializable(typeof(ConnectorDisconnectResult))] [JsonSerializable(typeof(ConnectorReconcileRequest))] +[JsonSerializable(typeof(ConnectorReconcileRequestWithSession))] [JsonSerializable(typeof(ConnectorRuntimeStatus))] +[JsonSerializable(typeof(ConnectorSessionAccount))] [JsonSerializable(typeof(ConnectorStatus))] [JsonSerializable(typeof(ContentExclusionCheckPathsRequest))] [JsonSerializable(typeof(ContentExclusionCheckPathsResult))] @@ -48176,6 +50795,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))] @@ -48197,10 +50818,17 @@ 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))] +[JsonSerializable(typeof(EnvironmentsGetRequest))] +[JsonSerializable(typeof(EnvironmentsGetResult))] +[JsonSerializable(typeof(EnvironmentsListRequest))] +[JsonSerializable(typeof(EnvironmentsListResult))] [JsonSerializable(typeof(EventLogReadRequest))] [JsonSerializable(typeof(EventLogReleaseInterestResult))] [JsonSerializable(typeof(EventLogTailResult))] @@ -48222,6 +50850,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(FolderTrustAddParams))] [JsonSerializable(typeof(FolderTrustCheckParams))] [JsonSerializable(typeof(FolderTrustCheckResult))] +[JsonSerializable(typeof(GitHubEnvironment))] [JsonSerializable(typeof(GitHubTelemetryClientInfo))] [JsonSerializable(typeof(GitHubTelemetryEvent))] [JsonSerializable(typeof(GitHubTelemetryNotification))] @@ -48254,6 +50883,10 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(HostDisposeResult))] [JsonSerializable(typeof(HostExitedRequest))] [JsonSerializable(typeof(HostGetConfigurationResult))] +[JsonSerializable(typeof(HostGetEnvironmentCredentialsResult))] +[JsonSerializable(typeof(HostGitHubEnvironmentOptions))] +[JsonSerializable(typeof(HostLocalServerConfiguration))] +[JsonSerializable(typeof(HostLocalServerOptions))] [JsonSerializable(typeof(HostPublishSessionRequest))] [JsonSerializable(typeof(HostPublishSessionResult))] [JsonSerializable(typeof(HostReadyRequest))] @@ -48297,8 +50930,21 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(LogRequest))] [JsonSerializable(typeof(LogResult))] [JsonSerializable(typeof(LspInitializeRequest))] +[JsonSerializable(typeof(ManagedSettingMeta))] +[JsonSerializable(typeof(ManagedSettingsComposeLayer))] +[JsonSerializable(typeof(ManagedSettingsComposeRequest))] +[JsonSerializable(typeof(ManagedSettingsComposeResult))] +[JsonSerializable(typeof(ManagedSettingsDiagnostic))] +[JsonSerializable(typeof(ManagedSettingsLayer))] +[JsonSerializable(typeof(ManagedSettingsMeta))] [JsonSerializable(typeof(ManagedSettingsReadResult))] +[JsonSerializable(typeof(ManagedSettingsResolveRequest))] +[JsonSerializable(typeof(ManagedSettingsResolveResult))] [JsonSerializable(typeof(ManagedSettingsResolvedData))] +[JsonSerializable(typeof(ManagedSettingsSchemaResult))] +[JsonSerializable(typeof(ManagedSettingsValidateRequest))] +[JsonSerializable(typeof(ManagedSettingsValidateResult))] +[JsonSerializable(typeof(ManagedSettingsValues))] [JsonSerializable(typeof(MarketplaceAddResult))] [JsonSerializable(typeof(MarketplaceBrowseResult))] [JsonSerializable(typeof(MarketplaceInfo))] @@ -48334,6 +50980,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))] @@ -48369,6 +51018,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))] @@ -48394,6 +51044,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))] @@ -48456,6 +51114,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))] @@ -48474,8 +51133,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))] @@ -48598,6 +51274,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ProviderSyncResult))] [JsonSerializable(typeof(ProviderTokenAcquireRequest))] [JsonSerializable(typeof(ProviderTokenAcquireResult))] +[JsonSerializable(typeof(ProviderWithdrawRequest))] +[JsonSerializable(typeof(ProviderWithdrawResult))] [JsonSerializable(typeof(PushAttachment))] [JsonSerializable(typeof(PushAttachmentFileLineRange))] [JsonSerializable(typeof(PushAttachmentGitHubFileDiffSide))] @@ -48671,6 +51349,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))] @@ -48714,6 +51395,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionCommandsListRequestWithSession))] [JsonSerializable(typeof(SessionCompletionItem))] [JsonSerializable(typeof(SessionCompletionsGetTriggerCharactersRequest))] +[JsonSerializable(typeof(SessionConnectorsGetAccountRequest))] [JsonSerializable(typeof(SessionConnectorsGetCapabilitiesRequest))] [JsonSerializable(typeof(SessionConnectorsGetStatusRequest))] [JsonSerializable(typeof(SessionConnectorsWithdrawProjectionRequest))] @@ -48728,6 +51410,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))] @@ -48750,6 +51434,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))] @@ -48778,6 +51463,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))] @@ -48816,6 +51502,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))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 4daefb3314..993035e6eb 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")] @@ -69,6 +71,7 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(McpResourcesListChangedEvent), "mcp.resources.list_changed")] [JsonDerivedType(typeof(McpToolsListChangedEvent), "mcp.tools.list_changed")] [JsonDerivedType(typeof(ModelCallFailureEvent), "model.call_failure")] +[JsonDerivedType(typeof(ModelCallFinalResultEvent), "model.call_final_result")] [JsonDerivedType(typeof(ModelCallFinishedEvent), "model.call_finished")] [JsonDerivedType(typeof(ModelCallStartEvent), "model.call_start")] [JsonDerivedType(typeof(PendingMessagesModifiedEvent), "pending_messages.modified")] @@ -165,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")] @@ -205,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() => @@ -215,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 @@ -1058,6 +1575,19 @@ public sealed partial class ModelCallFailureEvent : SessionEvent public required ModelCallFailureData Data { get; set; } } +/// Internal telemetry result for one logical model operation after all orchestrator-owned retries settle. +/// Represents the model.call_final_result event. +public sealed partial class ModelCallFinalResultEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "model.call_final_result"; + + /// The model.call_final_result event payload. + [JsonPropertyName("data")] + public required ModelCallFinalResultData Data { get; set; } +} + /// Final lifecycle outcome for one logical model dispatch. A logical dispatch may include internal reconnect or fallback work, so event count is not provider HTTP-request count. /// Represents the model.call_finished event. public sealed partial class ModelCallFinishedEvent : SessionEvent @@ -1123,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 { /// @@ -1136,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 @@ -1798,7 +2347,7 @@ public sealed partial class SessionAutoModeResolvedEvent : SessionEvent public required SessionAutoModeResolvedData Data { get; set; } } -/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +/// Effective enterprise managed settings and contributing channels. Session events report applied policy; sessionless resolve reports an account/device snapshot, and compose reports a non-applying preview of candidate documents. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively. Session-local SDK-client policy is included only in session results. Marked experimental while the managed-settings surface stabilizes. /// Represents the session.managed_settings_resolved event. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed partial class SessionManagedSettingsResolvedEvent : SessionEvent @@ -1878,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 @@ -2232,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")] @@ -2312,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")] @@ -2633,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")] @@ -4146,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")] @@ -4235,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")] @@ -4292,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] @@ -4403,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] @@ -4424,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")] @@ -4611,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] @@ -4654,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)] @@ -4721,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")] @@ -4772,6 +5381,23 @@ public sealed partial class ModelCallFailureData public ModelCallFailureTransport? Transport { get; set; } } +/// Internal telemetry result for one logical model operation after all orchestrator-owned retries settle. +public sealed partial class ModelCallFinalResultData +{ + /// Whether the final attempt used a bring-your-own-key provider. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("isByok")] + public bool? IsByok { get; set; } + + /// Model identifier used by the final attempt. + [JsonPropertyName("model")] + public required string Model { get; set; } + + /// Bounded result of the final attempt. + [JsonPropertyName("result")] + public required ModelCallFinalResult Result { get; set; } +} + /// Final lifecycle outcome for one logical model dispatch. A logical dispatch may include internal reconnect or fallback work, so event count is not provider HTTP-request count. public sealed partial class ModelCallFinishedData { @@ -4911,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")] @@ -4950,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; } @@ -4962,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 { @@ -4969,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; } @@ -4982,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)] @@ -5013,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")] @@ -6396,13 +7065,18 @@ 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")] public bool? StickyOverride { get; set; } } -/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +/// Effective enterprise managed settings and contributing channels. Session events report applied policy; sessionless resolve reports an account/device snapshot, and compose reports a non-applying preview of candidate documents. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively. Session-local SDK-client policy is included only in session results. Marked experimental while the managed-settings surface stabilizes. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] public sealed partial class SessionManagedSettingsResolvedData { @@ -6556,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 { @@ -6664,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; } @@ -8421,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; } @@ -8743,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 @@ -8983,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 { @@ -12167,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 @@ -16233,6 +17058,85 @@ public override void Write(Utf8JsonWriter writer, ModelCallFailureSource value, } } +/// Final bounded result of one logical model operation after its internal retry loop settles. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelCallFinalResult : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelCallFinalResult(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The final attempt succeeded. + public static ModelCallFinalResult Success { get; } = new("success"); + + /// The final attempt failed with HTTP 400. + public static ModelCallFinalResult Http400 { get; } = new("http_400"); + + /// The final attempt failed with HTTP 413. + public static ModelCallFinalResult Http413 { get; } = new("http_413"); + + /// The final attempt failed with HTTP 429. + public static ModelCallFinalResult Http429 { get; } = new("http_429"); + + /// The final attempt failed with another HTTP 4xx status. + public static ModelCallFinalResult Http4xx { get; } = new("http_4xx"); + + /// The final attempt failed with an HTTP 5xx status. + public static ModelCallFinalResult Http5xx { get; } = new("http_5xx"); + + /// The final attempt failed in the request transport. + public static ModelCallFinalResult TransportError { get; } = new("transport_error"); + + /// The final attempt failed without another bounded classification. + public static ModelCallFinalResult OtherError { get; } = new("other_error"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelCallFinalResult left, ModelCallFinalResult right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelCallFinalResult left, ModelCallFinalResult right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ModelCallFinalResult other && Equals(other); + + /// + public bool Equals(ModelCallFinalResult 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 ModelCallFinalResult Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ModelCallFinalResult value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelCallFinalResult)); + } + } +} + /// Final outcome of one logical model dispatch after response acceptance processing. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -16565,6 +17469,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}")] @@ -20130,6 +21163,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}")] @@ -20374,7 +21471,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. @@ -20692,6 +21789,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))] @@ -20719,6 +21822,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(ModelCallFailureData))] [JsonSerializable(typeof(ModelCallFailureEvent))] [JsonSerializable(typeof(ModelCallFailureRequestFingerprint))] +[JsonSerializable(typeof(ModelCallFinalResultData))] +[JsonSerializable(typeof(ModelCallFinalResultEvent))] [JsonSerializable(typeof(ModelCallFinishedData))] [JsonSerializable(typeof(ModelCallFinishedEvent))] [JsonSerializable(typeof(ModelCallStartData))] @@ -20850,6 +21955,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))] @@ -21004,6 +22110,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))] @@ -21030,6 +22137,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/Host.cs b/dotnet/src/Host.cs index 1de904473e..5835ea3a4c 100644 --- a/dotnet/src/Host.cs +++ b/dotnet/src/Host.cs @@ -32,18 +32,17 @@ public sealed record AhpSessionCreateRequest(SessionConfig Config, CancellationT [Experimental(Diagnostics.Experimental)] public sealed record AhpSessionResumeRequest(string SessionId, ResumeSessionConfig Config, CancellationToken CancellationToken); -/// Listener options and local application callbacks for in-process AHP hosting. +/// +/// Transport options and local application callbacks for in-process AHP hosting. +/// Select at least one transport explicitly; no local listener is started by default. +/// [Experimental(Diagnostics.Experimental)] public sealed class AhpHostOptions { - /// Bind address; defaults to loopback. - public string? Hostname { get; init; } - /// Listener port; omitted or zero selects an available port. - public int? Port { get; init; } - /// Connection token; omitted generates one when authentication is enabled. - public string? Token { get; init; } - /// Whether to require a connection token; defaults to true. - public bool? RequireConnectionToken { get; init; } + /// Local WebSocket transport settings; absent disables the local listener. + public HostLocalServerOptions? LocalServer { get; init; } + /// GitHub Mission Control transport settings, with required Name and ComputeId; absent disables it. + public HostGitHubEnvironmentOptions? GitHubEnvironment { get; init; } /// Creates the exact requested session on the owning client. public Func>? CreateSession { get; init; } /// Resumes the exact requested session on the owning client. @@ -70,17 +69,20 @@ internal AhpHost(HostStartResult info, ServerHostApi rpc) Url = info.Url; Token = info.Token; Pid = info.Pid; + EnvironmentId = info.EnvironmentId; _rpc = rpc; } /// The listener identity. public string HostId { get; } - /// The bound AHP WebSocket address. - public string Url { get; } - /// The connection token, absent when authentication is disabled. + /// The bound AHP WebSocket address, absent without a local server. + public string? Url { get; } + /// The connection token, absent without an authenticated local server. public string? Token { get; } /// Legacy companion PID; absent for in-process listeners, not the runtime PID. public long? Pid { get; } + /// The GitHub Mission Control environment ID, absent without a GitHub environment. + public string? EnvironmentId { get; } /// Publishes a resident session without invoking factories or transferring ownership. public Task PublishSessionAsync(string sessionId, CancellationToken cancellationToken = default) @@ -100,21 +102,36 @@ public sealed partial class CopilotClient private readonly Dictionary _ahpHandoffs = []; /// - /// Starts a full AHP listener in the connected runtime, without a companion process. + /// Starts AHP hosting in the connected runtime, without a companion process. + /// At least one of LocalServer or GitHubEnvironment must be selected explicitly. /// Its lifetime belongs to this connection, not the global runtime. /// Cancellation abandons the wait; a listener that subsequently starts is disposed. /// [Experimental(Diagnostics.Experimental)] - public async Task StartAhpHostAsync(AhpHostOptions? options = null, CancellationToken cancellationToken = default) + public async Task StartAhpHostAsync(AhpHostOptions options, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(options); + var localServer = options.LocalServer is { } local ? new HostLocalServerOptions + { + Hostname = local.Hostname, + Port = local.Port, + Token = local.Token, + RequireConnectionToken = local.RequireConnectionToken + } : null; + var githubEnvironment = options.GitHubEnvironment is { } github ? new HostGitHubEnvironmentOptions + { + Name = github.Name, + ComputeId = github.ComputeId + } : null; + if (localServer is null && githubEnvironment is null) + throw new ArgumentException("At least one AHP transport must be configured", nameof(options)); var connection = await EnsureConnectedAsync(cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - options ??= new(); var hostId = Guid.NewGuid().ToString(); lock (_ahpGate) _ahpHosts.Add(hostId, options); var rpc = connection.Server.Host; - var startup = rpc.StartAsync(hostId, options.Hostname, options.Port, options.Token, - options.RequireConnectionToken, options.CreateSession is not null ? true : null, + var startup = rpc.StartAsync(hostId, localServer, githubEnvironment, + options.CreateSession is not null ? true : null, options.ResumeSession is not null ? true : null, CancellationToken.None); try { 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..343bd680da 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); @@ -79,6 +79,7 @@ private sealed record EventSubscription(Type EventType, Action Han 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 +126,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. /// @@ -602,14 +606,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 +761,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 +1204,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) { @@ -1864,6 +2018,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 }; } @@ -2260,7 +2424,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; @@ -2370,6 +2534,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..7ed04c279b 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2052,6 +2052,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 +2267,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 +2453,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 +2570,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 +2749,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. /// @@ -4044,6 +4231,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 +4258,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 +4272,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 +4283,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. 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 @@ -680,7 +680,6 @@ did not produce the multi-release output. Re-build on JDK 25+ and verify the **/ErgonomicToolDefinitionIT.java **/GitHubTelemetryForwardingIT.java **/LowLevelToolDefinitionIT.java - **/RewindIT.java **/SystemMessageSectionsIT.java diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseCompletedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseCompletedEvent.java index d317f93a0e..27b144b281 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseCompletedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseCompletedEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** @@ -47,6 +48,7 @@ public record AssistantFusionPhaseCompletedEventData( /** Concrete model that executed the phase. */ @JsonProperty("model") String model, /** Explicit reasoning effort selected for this phase, if supplied. */ + @CopilotExperimental @JsonProperty("reasoningEffort") String reasoningEffort, /** Durable outcome status of the phase. */ @JsonProperty("status") FusionPhaseStatus status, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseFailedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseFailedEvent.java index c222e0e0c3..e248d0a9fe 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseFailedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseFailedEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** @@ -47,6 +48,7 @@ public record AssistantFusionPhaseFailedEventData( /** Concrete model that attempted the phase. */ @JsonProperty("model") String model, /** Explicit reasoning effort selected for this phase, if supplied. */ + @CopilotExperimental @JsonProperty("reasoningEffort") String reasoningEffort, /** Durable outcome status of the phase. */ @JsonProperty("status") FusionPhaseStatus status, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseStartedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseStartedEvent.java index 9408e87c2b..28ed76f283 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseStartedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseStartedEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** @@ -49,6 +50,7 @@ public record AssistantFusionPhaseStartedEventData( /** Concrete model executing the phase. */ @JsonProperty("model") String model, /** Explicit reasoning effort selected for this phase, if supplied. */ + @CopilotExperimental @JsonProperty("reasoningEffort") String reasoningEffort ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageEvent.java index d3154846ed..365b945965 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; @@ -82,8 +83,10 @@ public record AssistantMessageEventData( /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId, /** Provider-agnostic citations linking spans of this message's content to the sources that support them. Experimental; only populated when citation emission is enabled. */ + @CopilotExperimental @JsonProperty("citations") Citations citations, /** Experimental HydraFusion source attribution for this ordinary authoritative assistant message. */ + @CopilotExperimental @JsonProperty("fusion") FusionAttribution fusion ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java index 3603e09f1e..e9b97f00f3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java @@ -29,6 +29,8 @@ public record AssistantMessageReasoningBlocks( /** Model provider that produced these reasoning blocks. */ @JsonProperty("provider") String provider, /** 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. */ - @JsonProperty("blocks") List blocks + @JsonProperty("blocks") List blocks, + /** 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. */ + @JsonProperty("orderedBlocks") List orderedBlocks ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantUsageEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantUsageEvent.java index a2e08bfb14..0c02f5d15a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantUsageEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantUsageEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.time.OffsetDateTime; import java.util.List; import java.util.Map; @@ -56,6 +57,7 @@ public record AssistantUsageEventData( /** Recognized provider-reported reasons for dropped thinking blocks, in response order */ @JsonProperty("thinkingDroppedReasons") List thinkingDroppedReasons, /** Model multiplier cost for billing purposes */ + @CopilotExperimental @JsonProperty("cost") Double cost, /** Duration of the API call in milliseconds */ @JsonProperty("duration") Long duration, @@ -71,6 +73,10 @@ public record AssistantUsageEventData( @JsonProperty("interactionType") String interactionType, /** Whether this model call used a bring-your-own-key provider */ @JsonProperty("isByok") Boolean isByok, + /** 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. */ + @JsonProperty("byokKind") String byokKind, + /** 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. */ + @JsonProperty("modelProvider") String modelProvider, /** Whether Auto mode was selected for this model call */ @JsonProperty("isAuto") Boolean isAuto, /** Effective maximum prompt-token limit used for this model call */ @@ -122,6 +128,7 @@ public record AssistantUsageEventData( /** Whether the model response was blocked or truncated by content filtering (finish_reason === 'content_filter'). For Anthropic models this corresponds to a 'refusal' stop reason. */ @JsonProperty("contentFilterTriggered") Boolean contentFilterTriggered, /** Experimental HydraFusion attribution for this concrete model call's usage. */ + @CopilotExperimental @JsonProperty("fusion") FusionAttribution fusion ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseActor.java b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseActor.java new file mode 100644 index 0000000000..ef66119adf --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseActor.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Controlled provenance for a typed runtime response. Only `human_response`, minted by a trusted direct-interaction ingress, is human authorization evidence. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum HumanResponseActor { + /** The {@code human_response} variant. */ + HUMAN_RESPONSE("human_response"), + /** The {@code host_automation} variant. */ + HOST_AUTOMATION("host_automation"), + /** The {@code unknown} variant. */ + UNKNOWN("unknown"); + + private final String value; + HumanResponseActor(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static HumanResponseActor fromValue(String value) { + for (HumanResponseActor v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown HumanResponseActor value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedEvent.java new file mode 100644 index 0000000000..f2a2aa9012 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedEvent.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "human_response.recorded". Durable request-correlated evidence for a typed response to a runtime-owned question or plan review. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class HumanResponseRecordedEvent extends SessionEvent { + + @Override + public String getType() { return "human_response.recorded"; } + + @JsonProperty("data") + private HumanResponseRecordedEventData data; + + public HumanResponseRecordedEventData getData() { return data; } + public void setData(HumanResponseRecordedEventData data) { this.data = data; } + + /** Data payload for {@link HumanResponseRecordedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record HumanResponseRecordedEventData( + /** Request ID of the runtime-owned question or plan review. */ + @JsonProperty("requestId") String requestId, + /** Tool call ID that opened the request, when present. */ + @JsonProperty("toolCallId") String toolCallId, + /** Controlled actor provenance established at response ingress. */ + @JsonProperty("actor") HumanResponseActor actor, + /** Typed request and response payload. */ + @JsonProperty("response") HumanResponseRecordedResponse response + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponse.java similarity index 52% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponse.java index 582d7036d3..65efb2e2e0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponse.java @@ -5,35 +5,32 @@ // AUTO-GENERATED FILE - DO NOT EDIT // Generated from: api.schema.json -package com.github.copilot.generated.rpc; +package com.github.copilot.generated; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** - * Result of a OneAuth token acquisition. - * - * @apiNote This type is experimental and may change in a future version. + * Exact runtime-owned question or reviewed plan paired with the typed response that settled it. * * @since 1.0.0 */ -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "status", visible = true) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "responseKind", visible = true) @JsonSubTypes({ - @JsonSubTypes.Type(value = EntraTokenAcquireResultOk.class, name = "ok"), - @JsonSubTypes.Type(value = EntraTokenAcquireResultInteractionRequired.class, name = "interaction-required") + @JsonSubTypes.Type(value = HumanResponseRecordedResponseAskUser.class, name = "ask_user"), + @JsonSubTypes.Type(value = HumanResponseRecordedResponseUserInput.class, name = "user_input"), + @JsonSubTypes.Type(value = HumanResponseRecordedResponseExitPlanMode.class, name = "exit_plan_mode") }) -@CopilotExperimental @JsonIgnoreProperties(ignoreUnknown = true) @javax.annotation.processing.Generated("copilot-sdk-codegen") -public abstract class EntraTokenAcquireResult { +public abstract class HumanResponseRecordedResponse { /** * Returns the discriminator value for this variant. * - * @return the status discriminator + * @return the responseKind discriminator */ - public abstract String getStatus(); + public abstract String getResponseKind(); } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseAskUser.java b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseAskUser.java new file mode 100644 index 0000000000..03afda0422 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseAskUser.java @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Variant {@code ask_user} of {@link HumanResponseRecordedResponse}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class HumanResponseRecordedResponseAskUser extends HumanResponseRecordedResponse { + + @JsonProperty("responseKind") + private final String responseKind = "ask_user"; + + @Override + public String getResponseKind() { return responseKind; } + + /** Exact answer content accepted from the user. */ + @JsonProperty("content") + private Map content; + + /** Exact question displayed to the user. */ + @JsonProperty("message") + private String message; + + /** Exact response schema displayed to the user. */ + @JsonProperty("requestedSchema") + private ElicitationRequestedSchema requestedSchema; + + public Map getContent() { return content; } + public void setContent(Map content) { this.content = content; } + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + + public ElicitationRequestedSchema getRequestedSchema() { return requestedSchema; } + public void setRequestedSchema(ElicitationRequestedSchema requestedSchema) { this.requestedSchema = requestedSchema; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseExitPlanMode.java b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseExitPlanMode.java new file mode 100644 index 0000000000..0d13b119da --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseExitPlanMode.java @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Variant {@code exit_plan_mode} of {@link HumanResponseRecordedResponse}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class HumanResponseRecordedResponseExitPlanMode extends HumanResponseRecordedResponse { + + @JsonProperty("responseKind") + private final String responseKind = "exit_plan_mode"; + + @Override + public String getResponseKind() { return responseKind; } + + /** Exact plan summary displayed to the user. */ + @JsonProperty("summary") + private String summary; + + /** Exact full plan content available from the review UI. */ + @JsonProperty("planContent") + private String planContent; + + /** Actions offered by the plan review UI. */ + @JsonProperty("actions") + private List actions; + + /** Action the plan review UI recommended. */ + @JsonProperty("recommendedAction") + private ExitPlanModeAction recommendedAction; + + /** Whether the user approved the reviewed plan. */ + @JsonProperty("approved") + private Boolean approved; + + /** Action selected by the user, when applicable. */ + @JsonProperty("selectedAction") + private ExitPlanModeAction selectedAction; + + /** Whether the selected response requested edit auto-approval. */ + @JsonProperty("autoApproveEdits") + private Boolean autoApproveEdits; + + /** Exact feedback submitted with the plan decision, when present. */ + @JsonProperty("feedback") + private String feedback; + + public String getSummary() { return summary; } + public void setSummary(String summary) { this.summary = summary; } + + public String getPlanContent() { return planContent; } + public void setPlanContent(String planContent) { this.planContent = planContent; } + + public List getActions() { return actions; } + public void setActions(List actions) { this.actions = actions; } + + public ExitPlanModeAction getRecommendedAction() { return recommendedAction; } + public void setRecommendedAction(ExitPlanModeAction recommendedAction) { this.recommendedAction = recommendedAction; } + + public Boolean getApproved() { return approved; } + public void setApproved(Boolean approved) { this.approved = approved; } + + public ExitPlanModeAction getSelectedAction() { return selectedAction; } + public void setSelectedAction(ExitPlanModeAction selectedAction) { this.selectedAction = selectedAction; } + + public Boolean getAutoApproveEdits() { return autoApproveEdits; } + public void setAutoApproveEdits(Boolean autoApproveEdits) { this.autoApproveEdits = autoApproveEdits; } + + public String getFeedback() { return feedback; } + public void setFeedback(String feedback) { this.feedback = feedback; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseUserInput.java b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseUserInput.java new file mode 100644 index 0000000000..2845d6169a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/HumanResponseRecordedResponseUserInput.java @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Variant {@code user_input} of {@link HumanResponseRecordedResponse}. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class HumanResponseRecordedResponseUserInput extends HumanResponseRecordedResponse { + + @JsonProperty("responseKind") + private final String responseKind = "user_input"; + + @Override + public String getResponseKind() { return responseKind; } + + /** Exact question displayed to the user. */ + @JsonProperty("question") + private String question; + + /** Exact choices displayed to the user, when the request offered choices. */ + @JsonProperty("choices") + private List choices; + + /** Whether the displayed request allowed a free-form answer. */ + @JsonProperty("allowFreeform") + private Boolean allowFreeform; + + /** Exact selected or free-form answer submitted by the user. */ + @JsonProperty("answer") + private String answer; + + /** Whether the answer was typed as free-form text rather than selected from the displayed choices. */ + @JsonProperty("wasFreeform") + private Boolean wasFreeform; + + public String getQuestion() { return question; } + public void setQuestion(String question) { this.question = question; } + + public List getChoices() { return choices; } + public void setChoices(List choices) { this.choices = choices; } + + public Boolean getAllowFreeform() { return allowFreeform; } + public void setAllowFreeform(Boolean allowFreeform) { this.allowFreeform = allowFreeform; } + + public String getAnswer() { return answer; } + public void setAnswer(String answer) { this.answer = answer; } + + public Boolean getWasFreeform() { return wasFreeform; } + public void setWasFreeform(Boolean wasFreeform) { this.wasFreeform = wasFreeform; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFailureEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFailureEvent.java index 8e3e166f04..05e8a8371f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFailureEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFailureEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.Map; import javax.annotation.processing.Generated; @@ -65,6 +66,10 @@ public record ModelCallFailureEventData( @JsonProperty("maxOutputTokens") Long maxOutputTokens, /** Whether the failed call used a bring-your-own-key provider */ @JsonProperty("isByok") Boolean isByok, + /** 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. */ + @JsonProperty("byokKind") String byokKind, + /** 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. */ + @JsonProperty("modelProvider") String modelProvider, /** Whether the session selected Auto mode for the failed call */ @JsonProperty("isAuto") Boolean isAuto, /** Reasoning effort level used for the failed model call, if applicable */ @@ -86,6 +91,7 @@ public record ModelCallFailureEventData( /** Content-free structural summary of the failing request. Contains only counts and shape flags (no prompt content), so it is safe for unrestricted telemetry. Populated only for client-error (4xx) failures. */ @JsonProperty("requestFingerprint") ModelCallFailureRequestFingerprint requestFingerprint, /** Experimental HydraFusion attribution for this failed concrete model call. */ + @CopilotExperimental @JsonProperty("fusion") FusionAttribution fusion ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFinalResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFinalResult.java new file mode 100644 index 0000000000..39db478f12 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFinalResult.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Final bounded result of one logical model operation after its internal retry loop settles + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelCallFinalResult { + /** The {@code success} variant. */ + SUCCESS("success"), + /** The {@code http_400} variant. */ + HTTP_400("http_400"), + /** The {@code http_413} variant. */ + HTTP_413("http_413"), + /** The {@code http_429} variant. */ + HTTP_429("http_429"), + /** The {@code http_4xx} variant. */ + HTTP_4XX("http_4xx"), + /** The {@code http_5xx} variant. */ + HTTP_5XX("http_5xx"), + /** The {@code transport_error} variant. */ + TRANSPORT_ERROR("transport_error"), + /** The {@code other_error} variant. */ + OTHER_ERROR("other_error"); + + private final String value; + ModelCallFinalResult(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelCallFinalResult fromValue(String value) { + for (ModelCallFinalResult v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelCallFinalResult value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFinalResultEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFinalResultEvent.java new file mode 100644 index 0000000000..14af7a846c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallFinalResultEvent.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "model.call_final_result". Internal telemetry result for one logical model operation after all orchestrator-owned retries settle + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ModelCallFinalResultEvent extends SessionEvent { + + @Override + public String getType() { return "model.call_final_result"; } + + @JsonProperty("data") + private ModelCallFinalResultEventData data; + + public ModelCallFinalResultEventData getData() { return data; } + public void setData(ModelCallFinalResultEventData data) { this.data = data; } + + /** Data payload for {@link ModelCallFinalResultEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ModelCallFinalResultEventData( + /** Model identifier used by the final attempt */ + @JsonProperty("model") String model, + /** Whether the final attempt used a bring-your-own-key provider */ + @JsonProperty("isByok") Boolean isByok, + /** Bounded result of the final attempt */ + @JsonProperty("result") ModelCallFinalResult result + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallStartEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallStartEvent.java index a3db0aadbe..0e76806f41 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallStartEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ModelCallStartEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** @@ -41,6 +42,7 @@ public record ModelCallStartEventData( /** Previous response or interaction identifier included in the model request, when present */ @JsonProperty("previousResponseId") String previousResponseId, /** Experimental HydraFusion attribution for this concrete model call. */ + @CopilotExperimental @JsonProperty("fusion") FusionAttribution fusion, /** Parent task tool call ID when this model call belongs to a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionAssentDetectedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionAssentDetectedEvent.java index 0f3453c45a..84ccf7473e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionAssentDetectedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionAssentDetectedEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** @@ -35,8 +36,10 @@ public final class PermissionAssentDetectedEvent extends SessionEvent { @JsonInclude(JsonInclude.Include.NON_NULL) public record PermissionAssentDetectedEventData( /** Permission request the likely assent may refer to. The runtime derives this from the preceding durable blocker; the human message and extraction model do not choose it. */ + @CopilotExperimental @JsonProperty("requestId") String requestId, /** Human turn whose text triggered the deterministic assent recognizer. */ + @CopilotExperimental @JsonProperty("turnIndex") Long turnIndex ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionCarriedForwardEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionCarriedForwardEvent.java index a89b59660d..e9126e2308 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionCarriedForwardEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionCarriedForwardEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** @@ -35,12 +36,16 @@ public final class PermissionCarriedForwardEvent extends SessionEvent { @JsonInclude(JsonInclude.Include.NON_NULL) public record PermissionCarriedForwardEventData( /** Authorization edge minted for this admission. Not a prompt id: no prompt was raised, so no client should expect a request with this id. */ + @CopilotExperimental @JsonProperty("requestId") String requestId, /** Tool call this admission authorizes. Its execution receipts the prior grant, which is how a single-effect approval is spent rather than carried forward again. */ + @CopilotExperimental @JsonProperty("toolCallId") String toolCallId, /** Identity of the prior authorization record that contained the proposal. */ + @CopilotExperimental @JsonProperty("recordId") String recordId, /** Always `authorization_carry_forward`. Stated explicitly so a consumer reading this event cannot mistake it for a human, host-policy, or assisted-approval decision. */ + @CopilotExperimental @JsonProperty("decisionSource") PermissionDecisionSource decisionSource ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionCompletedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionCompletedEvent.java index b6da6bfb05..2cafbcafb8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionCompletedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionCompletedEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** @@ -45,6 +46,7 @@ public record PermissionCompletedEventData( /** The result of the permission request */ @JsonProperty("result") Object result, /** Who decided this permission request. Absent on completions recorded before this field existed, which consumers must treat as "not a human decision" rather than assuming one. Authorization records are minted only for `human_response`; an assisted-approval verdict, a host policy, an unattended fallback, and a hook resolution all produce the same `result` a person does, so this is the only field that distinguishes them. */ + @CopilotExperimental @JsonProperty("decisionSource") PermissionDecisionSource decisionSource ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionContextualAuthorizationEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionContextualAuthorizationEvent.java index 0712d39db8..80bc8cd24e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionContextualAuthorizationEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionContextualAuthorizationEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** @@ -35,16 +36,22 @@ public final class PermissionContextualAuthorizationEvent extends SessionEvent { @JsonInclude(JsonInclude.Include.NON_NULL) public record PermissionContextualAuthorizationEventData( /** Deterministic identity of the contextual message grant. */ + @CopilotExperimental @JsonProperty("recordId") String recordId, /** Original blocked permission request selected by deterministic event ordering, never by the extraction model. */ + @CopilotExperimental @JsonProperty("requestId") String requestId, /** Human turn containing the contextual decision. */ + @CopilotExperimental @JsonProperty("turnIndex") Long turnIndex, /** Whether the contextual human span granted or denied authority. */ + @CopilotExperimental @JsonProperty("polarity") PermissionMessageAuthorizationPolarity polarity, /** Start byte offset of the contextual decision span within the turn. */ + @CopilotExperimental @JsonProperty("spanStart") Long spanStart, /** End byte offset of the contextual decision span within the turn. */ + @CopilotExperimental @JsonProperty("spanEnd") Long spanEnd ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationDegradedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationDegradedEvent.java index 8efdf509a6..dc49be3971 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationDegradedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationDegradedEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** @@ -35,6 +36,7 @@ public final class PermissionMessageAuthorizationDegradedEvent extends SessionEv @JsonInclude(JsonInclude.Include.NON_NULL) public record PermissionMessageAuthorizationDegradedEventData( /** The human turn that could not be represented safely. */ + @CopilotExperimental @JsonProperty("turnIndex") Long turnIndex ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationEvent.java index 3a6c5aa6f0..88ff87b9dd 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; @@ -36,22 +37,31 @@ public final class PermissionMessageAuthorizationEvent extends SessionEvent { @JsonInclude(JsonInclude.Include.NON_NULL) public record PermissionMessageAuthorizationEventData( /** Deterministic identity of the record, derived from the turn and span offsets so re-extracting the same span mints nothing new. */ + @CopilotExperimental @JsonProperty("recordId") String recordId, /** The human turn the quoted span was read from. */ + @CopilotExperimental @JsonProperty("turnIndex") Long turnIndex, /** Whether the claim granted or denied authority. */ + @CopilotExperimental @JsonProperty("polarity") PermissionMessageAuthorizationPolarity polarity, /** The kind of effect authorized, as an action-class identifier. */ + @CopilotExperimental @JsonProperty("actionClass") String actionClass, /** Start byte offset of the authorizing span within the turn. */ + @CopilotExperimental @JsonProperty("spanStart") Long spanStart, /** End byte offset of the authorizing span within the turn. */ + @CopilotExperimental @JsonProperty("spanEnd") Long spanEnd, /** Concrete named targets that appear verbatim inside the span. */ + @CopilotExperimental @JsonProperty("targetMembers") List targetMembers, /** The task the permission is scoped to, when the human named one. */ + @CopilotExperimental @JsonProperty("task") String task, /** The trusted version discriminator, when one exists. Exact shell-command grants carry the byte-identical commands grounded in the human span; world-derived classes carry a file object, remote tip, or runner only when that state was captured safely. An opaque object mirroring the runtime's adjacently-tagged resolution. */ + @CopilotExperimental @JsonProperty("world") Object world ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationReadEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationReadEvent.java index 7a372823c6..b6fb8626bf 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationReadEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionMessageAuthorizationReadEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** @@ -35,8 +36,10 @@ public final class PermissionMessageAuthorizationReadEvent extends SessionEvent @JsonInclude(JsonInclude.Include.NON_NULL) public record PermissionMessageAuthorizationReadEventData( /** The human turn that was read by the proposer. */ + @CopilotExperimental @JsonProperty("turnIndex") Long turnIndex, /** Whether this read activates ongoing message-backed extraction. False for a contextual-assent-only pass while auto-approval is off, so unrelated future messages remain outside extraction. */ + @CopilotExperimental @JsonProperty("activatesExtraction") Boolean activatesExtraction ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/PromptCacheBreakEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/PromptCacheBreakEvent.java index 7ad39dec97..f3a21532ba 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/PromptCacheBreakEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/PromptCacheBreakEvent.java @@ -67,6 +67,8 @@ public record PromptCacheBreakEventData( @JsonProperty("toolsRemovedRaw") List toolsRemovedRaw, /** Raw names of tools redefined since the prior call, restricted because a tool name can be user-authored */ @JsonProperty("toolsRedefinedRaw") List toolsRedefinedRaw, + /** Changed definition parts of redefined tools, as `tool:part` entries; property-level parts only for telemetry-safe tools, whose other names are hashed */ + @JsonProperty("toolsRedefinedParts") List toolsRedefinedParts, /** Whether the tool list kept its members but changed their order */ @JsonProperty("toolsReordered") Boolean toolsReordered, /** Names of the system-prompt segments whose content changed */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionAutoModeResolvedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionAutoModeResolvedEvent.java index 88b06f4471..ba26c98c2e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionAutoModeResolvedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionAutoModeResolvedEvent.java @@ -56,6 +56,8 @@ public record SessionAutoModeResolvedEventData( @JsonProperty("fallback") Boolean fallback, /** Server-provided reason for falling back, when available */ @JsonProperty("fallbackReason") String fallbackReason, + /** 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. */ + @JsonProperty("selectionReason") String selectionReason, /** Whether a sticky model choice overrode the router result */ @JsonProperty("stickyOverride") Boolean stickyOverride, /** Server-reported router processing time in milliseconds */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java index 864072a110..726dd21975 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java @@ -89,12 +89,14 @@ @JsonSubTypes.Type(value = AssistantUsageEvent.class, name = "assistant.usage"), @JsonSubTypes.Type(value = PromptCacheBreakEvent.class, name = "prompt_cache_break"), @JsonSubTypes.Type(value = ModelCallFailureEvent.class, name = "model.call_failure"), + @JsonSubTypes.Type(value = ModelCallFinalResultEvent.class, name = "model.call_final_result"), @JsonSubTypes.Type(value = ModelCallFinishedEvent.class, name = "model.call_finished"), @JsonSubTypes.Type(value = ModelCallStartEvent.class, name = "model.call_start"), @JsonSubTypes.Type(value = AbortEvent.class, name = "abort"), @JsonSubTypes.Type(value = ToolUserRequestedEvent.class, name = "tool.user_requested"), @JsonSubTypes.Type(value = ToolExecutionStartEvent.class, name = "tool.execution_start"), @JsonSubTypes.Type(value = ToolExecutionPartialResultEvent.class, name = "tool.execution_partial_result"), + @JsonSubTypes.Type(value = ToolShellOutputEvent.class, name = "tool.shell_output"), @JsonSubTypes.Type(value = ToolExecutionProgressEvent.class, name = "tool.execution_progress"), @JsonSubTypes.Type(value = ToolExecutionCompleteEvent.class, name = "tool.execution_complete"), @JsonSubTypes.Type(value = ToolSearchActivatedEvent.class, name = "tool_search.activated"), @@ -151,6 +153,7 @@ @JsonSubTypes.Type(value = CapabilitiesChangedEvent.class, name = "capabilities.changed"), @JsonSubTypes.Type(value = ExitPlanModeRequestedEvent.class, name = "exit_plan_mode.requested"), @JsonSubTypes.Type(value = ExitPlanModeCompletedEvent.class, name = "exit_plan_mode.completed"), + @JsonSubTypes.Type(value = HumanResponseRecordedEvent.class, name = "human_response.recorded"), @JsonSubTypes.Type(value = SessionToolsUpdatedEvent.class, name = "session.tools_updated"), @JsonSubTypes.Type(value = SessionBackgroundTasksChangedEvent.class, name = "session.background_tasks_changed"), @JsonSubTypes.Type(value = WorkflowRunUpdatedEvent.class, name = "workflow.run_updated"), @@ -241,12 +244,14 @@ public abstract sealed class SessionEvent permits AssistantUsageEvent, PromptCacheBreakEvent, ModelCallFailureEvent, + ModelCallFinalResultEvent, ModelCallFinishedEvent, ModelCallStartEvent, AbortEvent, ToolUserRequestedEvent, ToolExecutionStartEvent, ToolExecutionPartialResultEvent, + ToolShellOutputEvent, ToolExecutionProgressEvent, ToolExecutionCompleteEvent, ToolSearchActivatedEvent, @@ -303,6 +308,7 @@ public abstract sealed class SessionEvent permits CapabilitiesChangedEvent, ExitPlanModeRequestedEvent, ExitPlanModeCompletedEvent, + HumanResponseRecordedEvent, SessionToolsUpdatedEvent, SessionBackgroundTasksChangedEvent, WorkflowRunUpdatedEvent, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionFusionResolvedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionFusionResolvedEvent.java index 8bb0a5069b..44ef8be4ab 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionFusionResolvedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionFusionResolvedEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; @@ -64,18 +65,23 @@ public record SessionFusionResolvedEventData( /** Validated orchestration pattern selected for the turn. */ @JsonProperty("pattern") FusionPattern pattern, /** Short human-readable summary of the selected workflow, suitable for immediate client display after routing. May be absent in older durable events; omit the explanation or derive one from pattern and phasePlan. Display text, not a stable machine-readable value. */ + @CopilotExperimental @JsonProperty("hint") String hint, /** Presentation-neutral phase plan for clients that render workflow progress. */ + @CopilotExperimental @JsonProperty("phasePlan") List phasePlan, /** Concrete model selected for the primary solver phase. */ @JsonProperty("primaryModel") String primaryModel, /** Concrete model selected for Critique review, or the legacy Cascade judge/repair model when role-specific fields are absent. */ @JsonProperty("secondaryModel") String secondaryModel, /** Concrete model selected for Cascade escalation-gate calls, when required. */ + @CopilotExperimental @JsonProperty("judgeModel") String judgeModel, /** Concrete model selected for Cascade repair, when required. */ + @CopilotExperimental @JsonProperty("repairModel") String repairModel, /** Planned Critique phase identities, including independent critics with repeated model IDs. May be absent in older events; consumers then use secondaryModel for the legacy critic. */ + @CopilotExperimental @JsonProperty("critics") List critics, /** Concrete model used when the planned primary model cannot execute. */ @JsonProperty("fallbackModel") String fallbackModel, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java index e5428d26db..d495ec8cf6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. + * Session event "session.managed_settings_resolved". Effective enterprise managed settings applied to the session and their contributing channels. Emitted whenever managed policy is applied or reapplied, including session start, resume, and account switch. This ephemeral live snapshot is delivered to subscribers but not persisted to the session event log; initial resolution occurs before session.start. * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerStatusChangedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerStatusChangedEvent.java index a1c411e297..b772a54131 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerStatusChangedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerStatusChangedEvent.java @@ -42,7 +42,7 @@ public record SessionMcpServerStatusChangedEventData( @JsonProperty("error") String error, /** Runtime-produced classification for the final failed connection; unclassified means no classification was supplied. Additional string values may be introduced. */ @JsonProperty("errorClassification") String errorClassification, - /** 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. */ @JsonProperty("configSource") String configSource ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java index 4ea1dbd46d..09e39c584d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java @@ -42,6 +42,8 @@ public record SessionModelChangeEventData( @JsonProperty("previousReasoningEffort") String previousReasoningEffort, /** Reasoning effort level after the model change, if applicable */ @JsonProperty("reasoningEffort") String reasoningEffort, + /** Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. */ + @JsonProperty("reasoningEffortModel") String reasoningEffortModel, /** Reasoning summary mode before the model change, if applicable */ @JsonProperty("previousReasoningSummary") ReasoningSummary previousReasoningSummary, /** Reasoning summary mode after the model change, if applicable */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionPermissionsChangedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionPermissionsChangedEvent.java index ef332617ac..5d45858942 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionPermissionsChangedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionPermissionsChangedEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** @@ -35,10 +36,13 @@ public final class SessionPermissionsChangedEvent extends SessionEvent { @JsonInclude(JsonInclude.Include.NON_NULL) public record SessionPermissionsChangedEventData( /** Permission mode before the change */ + @CopilotExperimental @JsonProperty("previousMode") PermissionMode previousMode, /** Permission mode after the change */ + @CopilotExperimental @JsonProperty("mode") PermissionMode mode, /** Explicit LLM judge model override used by assisted mode; omitted when the provider default applies */ + @CopilotExperimental @JsonProperty("assistedApprovalModel") String assistedApprovalModel ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java index de54f8a3ce..4120d9e7c8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java @@ -45,6 +45,8 @@ public record SessionResumeEventData( @JsonProperty("selectedModel") String selectedModel, /** Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") */ @JsonProperty("reasoningEffort") String reasoningEffort, + /** Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. */ + @JsonProperty("reasoningEffortModel") String reasoningEffortModel, /** Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") */ @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, /** Output verbosity level used for model calls, if applicable (e.g. "low", "medium", "high") */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionShutdownEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionShutdownEvent.java index cf8600e388..ab97f6b192 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionShutdownEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionShutdownEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import java.util.Map; import javax.annotation.processing.Generated; @@ -42,6 +43,7 @@ public record SessionShutdownEventData( /** Total number of premium API requests used during the session */ @JsonProperty("totalPremiumRequests") Double totalPremiumRequests, /** Session-wide accumulated nano-AI units cost */ + @CopilotExperimental @JsonProperty("totalNanoAiu") Double totalNanoAiu, /** Session-wide per-token-type accumulated token counts */ @JsonProperty("tokenDetails") Map tokenDetails, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionStartEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionStartEvent.java index b977ae036f..db7336ce68 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionStartEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionStartEvent.java @@ -49,6 +49,8 @@ public record SessionStartEventData( @JsonProperty("selectedModel") String selectedModel, /** Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") */ @JsonProperty("reasoningEffort") String reasoningEffort, + /** Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. */ + @JsonProperty("reasoningEffortModel") String reasoningEffortModel, /** Reasoning summary mode used for model calls, if applicable (e.g. "none", "concise", "detailed") */ @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, /** Output verbosity level used for model calls, if applicable (e.g. "low", "medium", "high") */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteEvent.java index 2b299df85b..1f2d180587 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteEvent.java @@ -10,6 +10,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; import java.util.Map; import javax.annotation.processing.Generated; @@ -42,6 +44,7 @@ public record ToolExecutionCompleteEventData( /** Model identifier that generated this tool call */ @JsonProperty("model") String model, /** FIDES IFC label projected from tool ingress metadata (MCP `CallToolResult._meta` or synthesized built-in ingress labels). Persisted as `{ ifc: ... }` so the label survives session resume, including model-visible failure results. Experimental. */ + @CopilotExperimental @JsonProperty("mcpMeta") Object mcpMeta, /** CAPI interaction ID for correlating this tool execution with upstream telemetry */ @JsonProperty("interactionId") String interactionId, @@ -62,10 +65,15 @@ public record ToolExecutionCompleteEventData( /** Whether this tool execution ran inside a sandbox container */ @JsonProperty("sandboxed") Boolean sandboxed, /** Experimental shell completion facts captured before the persisted result contents are stripped. */ + @CopilotExperimental @JsonProperty("shellExecution") ToolExecutionCompleteShellExecution shellExecution, + /** 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. */ + @CopilotExperimental + @JsonProperty("fileEdits") List fileEdits, /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId, /** Experimental HydraFusion attribution for this tool completion. */ + @CopilotExperimental @JsonProperty("fusion") FusionAttribution fusion ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteFileEdit.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteFileEdit.java new file mode 100644 index 0000000000..02a6db87d3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteFileEdit.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * A file mutation that was actually committed by a built-in file editing tool. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ToolExecutionCompleteFileEdit( + /** Absolute path in the session filesystem namespace. */ + @JsonProperty("path") String path, + /** Kind of mutation committed at this path. */ + @JsonProperty("kind") ToolExecutionCompleteFileEditKind kind +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteFileEditKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteFileEditKind.java new file mode 100644 index 0000000000..cdf2f3aa50 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteFileEditKind.java @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Kind of file mutation committed by a built-in editing tool. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ToolExecutionCompleteFileEditKind { + /** The {@code create} variant. */ + public static final ToolExecutionCompleteFileEditKind CREATE = new ToolExecutionCompleteFileEditKind("create"); + /** The {@code edit} variant. */ + public static final ToolExecutionCompleteFileEditKind EDIT = new ToolExecutionCompleteFileEditKind("edit"); + /** The {@code delete} variant. */ + public static final ToolExecutionCompleteFileEditKind DELETE = new ToolExecutionCompleteFileEditKind("delete"); + /** An explicit unknown file operation kind. */ + public static final ToolExecutionCompleteFileEditKind UNKNOWN = new ToolExecutionCompleteFileEditKind("unknown"); + + private final String value; + private ToolExecutionCompleteFileEditKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ToolExecutionCompleteFileEditKind fromValue(String value) { + if (value == null) throw new IllegalArgumentException("Missing ToolExecutionCompleteFileEditKind value"); + if ("create".equals(value)) return CREATE; + if ("edit".equals(value)) return EDIT; + if ("delete".equals(value)) return DELETE; + if ("unknown".equals(value)) return UNKNOWN; + return new ToolExecutionCompleteFileEditKind(value); + } + @Override + public boolean equals(Object other) { + return other instanceof ToolExecutionCompleteFileEditKind kind && value.equals(kind.value); + } + @Override + public int hashCode() { return value.hashCode(); } + @Override + public String toString() { return value; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionPartialResultEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionPartialResultEvent.java index e78d4d2a78..d8068f687a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionPartialResultEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionPartialResultEvent.java @@ -13,9 +13,10 @@ import javax.annotation.processing.Generated; /** - * 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. * @since 1.0.0 */ +@Deprecated @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) @javax.annotation.processing.Generated("copilot-sdk-codegen") @@ -31,12 +32,13 @@ public final class ToolExecutionPartialResultEvent extends SessionEvent { public void setData(ToolExecutionPartialResultEventData data) { this.data = data; } /** Data payload for {@link ToolExecutionPartialResultEvent}. */ + @Deprecated @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public record ToolExecutionPartialResultEventData( /** Tool call ID this partial result belongs to */ @JsonProperty("toolCallId") String toolCallId, - /** Incremental output chunk from the running tool */ + /** Merged replacement snapshot from the running shell, not an append-only chunk */ @JsonProperty("partialOutput") String partialOutput ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionProgressEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionProgressEvent.java index 51be395194..2857bead45 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionProgressEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionProgressEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** @@ -37,7 +38,10 @@ public record ToolExecutionProgressEventData( /** Tool call ID this progress notification belongs to */ @JsonProperty("toolCallId") String toolCallId, /** Human-readable progress status message (e.g., from an MCP server) */ - @JsonProperty("progressMessage") String progressMessage + @JsonProperty("progressMessage") String progressMessage, + /** Client-only structured progress metadata. Not model-facing tool output. */ + @CopilotExperimental + @JsonProperty("structuredContent") Object structuredContent ) { } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java index 8bd686beaf..2c0d67c3f2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** @@ -67,6 +68,7 @@ public record ToolExecutionStartEventData( /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId, /** Experimental HydraFusion attribution for this tool execution. */ + @CopilotExperimental @JsonProperty("fusion") FusionAttribution fusion ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolShellOutputEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolShellOutputEvent.java new file mode 100644 index 0000000000..39e496ddc7 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolShellOutputEvent.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * 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. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ToolShellOutputEvent extends SessionEvent { + + @Override + public String getType() { return "tool.shell_output"; } + + @JsonProperty("data") + private ToolShellOutputEventData data; + + public ToolShellOutputEventData getData() { return data; } + public void setData(ToolShellOutputEventData data) { this.data = data; } + + /** Data payload for {@link ToolShellOutputEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ToolShellOutputEventData( + /** Tool call ID that owns this shell output */ + @JsonProperty("toolCallId") String toolCallId, + /** Output source. Omission means stdout. Terminal output has no separate stdout/stderr attribution. */ + @JsonProperty("stream") ToolShellOutputStream stream, + /** New output to append, without synthetic shell-result markers or stream-switch separators */ + @JsonProperty("text") String text, + /** Zero-based publication sequence across all output streams for this tool call. Not a byte offset or an OS write-order guarantee. */ + @JsonProperty("sequence") Long sequence + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolShellOutputStream.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolShellOutputStream.java new file mode 100644 index 0000000000..6a424b0772 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolShellOutputStream.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Shell output source. Terminal output has no separate stdout/stderr attribution. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ToolShellOutputStream { + /** The {@code stdout} variant. */ + STDOUT("stdout"), + /** The {@code stderr} variant. */ + STDERR("stderr"), + /** The {@code terminal} variant. */ + TERMINAL("terminal"); + + private final String value; + ToolShellOutputStream(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ToolShellOutputStream fromValue(String value) { + for (ToolShellOutputStream v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ToolShellOutputStream value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountAuthInfo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountAuthInfo.java new file mode 100644 index 0000000000..b6a93fb206 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountAuthInfo.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * An interactive account whose model provider owns its credentials. It carries no GitHub credential. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AccountAuthInfo extends AuthInfo { + + @JsonProperty("type") + private final String type = "account"; + + @Override + public String getType() { return type; } + + /** Host coordinate owned by the account's model provider. */ + @JsonProperty("host") + private String host; + + /** Login identifying the provider-owned account. */ + @JsonProperty("login") + private String login; + + public String getHost() { return host; } + public void setHost(String host) { this.host = host; } + + public String getLogin() { return login; } + public void setLogin(String login) { this.login = login; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountsAcquireEntraTokenParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountsAcquireEntraTokenParams.java deleted file mode 100644 index 34f3cfa8a3..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AccountsAcquireEntraTokenParams.java +++ /dev/null @@ -1,41 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; -import java.util.List; -import javax.annotation.processing.Generated; - -/** - * OneAuth token request supplied by a trusted host application. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ -@CopilotExperimental -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record AccountsAcquireEntraTokenParams( - /** Public client application id. */ - @JsonProperty("clientId") String clientId, - /** Tenant id or tenant selector, such as common or organizations. */ - @JsonProperty("tenantId") String tenantId, - /** 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. */ - @JsonProperty("redirectUri") String redirectUri, - /** Exact delegated scopes to request. */ - @JsonProperty("scopes") List scopes, - /** Whether the broker may show interaction. */ - @JsonProperty("interaction") EntraTokenInteraction interaction, - /** Previously rejected token that OneAuth must bypass during renewal. */ - @JsonProperty("accessTokenToRenew") String accessTokenToRenew -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthIdentity.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthIdentity.java new file mode 100644 index 0000000000..dfeb3c023a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthIdentity.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Credential-free authentication identity safe to expose to hosts and user interfaces. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record AuthIdentity( + /** Authentication type */ + @JsonProperty("type") AuthInfoType type, + /** Authentication host */ + @JsonProperty("host") String host, + /** Authenticated login, when available */ + @JsonProperty("login") String login, + /** Name of the environment variable that supplied the credential, when applicable */ + @JsonProperty("envVar") String envVar, + /** Opaque SDK GitHub credential registration backing this identity. Routing metadata only; never a credential. */ + @JsonProperty("registrationId") String registrationId, + /** Snapshot of the authenticated user's Copilot subscription info, if known */ + @JsonProperty("copilotUser") CopilotUserResponse copilotUser +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthIdentityMetadata.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthIdentityMetadata.java new file mode 100644 index 0000000000..7ccad64df8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthIdentityMetadata.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Credential-free identity metadata. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AuthIdentityMetadata( + /** Authentication type. */ + @JsonProperty("type") AuthInfoType type, + /** Identity host. */ + @JsonProperty("host") String host, + /** User login. */ + @JsonProperty("login") String login +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfo.java index 5d82a47f85..7b03a6a29a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfo.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfo.java @@ -25,6 +25,7 @@ @JsonSubTypes.Type(value = TokenProviderAuthInfo.class, name = "token-provider"), @JsonSubTypes.Type(value = CopilotApiTokenAuthInfo.class, name = "copilot-api-token"), @JsonSubTypes.Type(value = UserAuthInfo.class, name = "user"), + @JsonSubTypes.Type(value = AccountAuthInfo.class, name = "account"), @JsonSubTypes.Type(value = GhCliAuthInfo.class, name = "gh-cli"), @JsonSubTypes.Type(value = ApiKeyAuthInfo.class, name = "api-key") }) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfoType.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfoType.java index 5f81c8cf84..1be60446c1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfoType.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthInfoType.java @@ -22,6 +22,8 @@ public enum AuthInfoType { ENV("env"), /** The {@code user} variant. */ USER("user"), + /** The {@code account} variant. */ + ACCOUNT("account"), /** The {@code gh-cli} variant. */ GH_CLI("gh-cli"), /** The {@code api-key} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginAccount.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginAccount.java new file mode 100644 index 0000000000..87405999ed --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginAccount.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * A credential-free account choice after sign-in. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AuthLoginAccount( + /** Opaque identifier supplied to the next login step to select this account. */ + @JsonProperty("selectionId") String selectionId, + /** Host coordinate owned by the selected account's provider. */ + @JsonProperty("host") String host, + /** Human-readable login for the account choice. */ + @JsonProperty("login") String login, + /** Provider kind that owns this account choice. */ + @JsonProperty("kind") AccountKind kind +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultDto.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultDto.java index cbcffb6e54..ecbb106f12 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultDto.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultDto.java @@ -11,10 +11,11 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.copilot.CopilotExperimental; +import java.util.List; import javax.annotation.processing.Generated; /** - * Terminal result of an interactive login flow. + * Result of an interactive login flow. Pending consent or account selection is not terminal. * * @apiNote This type is experimental and may change in a future version. * @@ -25,11 +26,13 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record AuthLoginResultDto( - /** Terminal disposition of the login. */ + /** Current disposition of the login, including pending user decisions. */ @JsonProperty("status") AuthLoginResultStatus status, /** Host that was signed in, when completed. */ @JsonProperty("host") String host, /** Login that was signed in, when completed. */ - @JsonProperty("login") String login + @JsonProperty("login") String login, + /** Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 first. */ + @JsonProperty("accounts") List accounts ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultStatus.java index 9e36ad0027..07becf2fb9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultStatus.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginResultStatus.java @@ -11,7 +11,7 @@ import javax.annotation.processing.Generated; /** - * Terminal disposition of a login persistence attempt. + * Disposition of a login attempt, including pending user decisions. * * @apiNote This type is experimental and may change in a future version. * @@ -24,6 +24,8 @@ public enum AuthLoginResultStatus { COMPLETED("completed"), /** The {@code needs-plaintext-consent} variant. */ NEEDS_PLAINTEXT_CONSENT("needs-plaintext-consent"), + /** The {@code needs-account-selection} variant. */ + NEEDS_ACCOUNT_SELECTION("needs-account-selection"), /** The {@code declined} variant. */ DECLINED("declined"); diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepCompleted.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepCompleted.java index b1fb524643..a8ea53e123 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepCompleted.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AuthLoginStepCompleted.java @@ -32,7 +32,7 @@ public final class AuthLoginStepCompleted extends AuthLoginStep { @Override public String getKind() { return kind; } - /** The terminal login result. */ + /** Login result. When status is needs-plaintext-consent or needs-account-selection, advance with the user's decision to continue. */ @JsonProperty("result") private AuthLoginResultDto result; diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsFinalizeInvocationEffectResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CommandsFinalizeInvocationEffectResult.java similarity index 89% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsFinalizeInvocationEffectResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/CommandsFinalizeInvocationEffectResult.java index 523a68564e..8718e93ebe 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsFinalizeInvocationEffectResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CommandsFinalizeInvocationEffectResult.java @@ -16,14 +16,15 @@ /** * Whether finalizing the invocation effect succeeded, and the failure reason when it did not. * - * @apiNote This method is experimental and may change in a future version. + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ @CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionCommandsFinalizeInvocationEffectResult( +record CommandsFinalizeInvocationEffectResult( /** Whether the pending invocation effect was finalized successfully. */ @JsonProperty("success") Boolean success, /** Failure reason when the invocation effect could not be finalized. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CommandsInvocationEffectOutcome.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CommandsInvocationEffectOutcome.java index 65293f1219..6a6bbbbb24 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CommandsInvocationEffectOutcome.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CommandsInvocationEffectOutcome.java @@ -19,7 +19,7 @@ */ @CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum CommandsInvocationEffectOutcome { +enum CommandsInvocationEffectOutcome { /** The {@code applied} variant. */ APPLIED("applied"), /** The {@code cancelled} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectClientInfo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectClientInfo.java index e5b6b6f24d..5dd5e0a2b2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectClientInfo.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectClientInfo.java @@ -20,7 +20,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record ConnectClientInfo( +record ConnectClientInfo( /** Name of the host editor, e.g. `"vscode"`. */ @JsonProperty("editorName") String editorName, /** Version of the host editor, e.g. `"1.124.2"`. Ignored unless it looks like a version string. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectParams.java index 6500bb49ee..01fbacd610 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectParams.java @@ -24,7 +24,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record ConnectParams( +record ConnectParams( /** Opt this connection in to GitHub telemetry forwarding for its lifetime. When set, the runtime forwards this host's telemetry across all its sessions, its sessionless events, and explicitly process-wide events over the `gitHubTelemetry.event` notification. Connections intentionally sharing one server receive that server's events; independently embedded runtime hosts do not receive each other's host-owned events. Regular events are also written to the runtime's normal GitHub/CTS path (dual-write); host-only compatibility events are forward-only and intentionally skip that path. Intended for first-party hosts that re-emit the events into their own telemetry stores. Both unrestricted and restricted events are forwarded, each tagged with a `restricted` discriminator; a backstop drops restricted events when restricted telemetry is disabled — using the process-global gate for ordinary events and an explicit session-scoped decision for host-only events. */ @JsonProperty("enableGitHubTelemetryForwarding") Boolean enableGitHubTelemetryForwarding, /** Identity of the integrating host. Optional; omit it to keep the default attribution. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectResult.java index 41a200ff4b..01764bfe25 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectResult.java @@ -10,21 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; /** * Handshake result reporting the server's protocol version and package version on success. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record ConnectResult( +record ConnectResult( /** Always true on success */ @JsonProperty("ok") Boolean ok, /** Server protocol version number */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogEntry.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogEntry.java index 53c3750792..0c50de6969 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogEntry.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorCatalogEntry.java @@ -32,9 +32,34 @@ public record ConnectorCatalogEntry( @JsonProperty("displayName") String displayName, /** Untrusted service description, when present. */ @JsonProperty("description") String description, + /** Optional catalog logo. */ + @JsonProperty("logo") String logo, + /** Optional catalog tier. */ + @JsonProperty("tier") String tier, + /** Optional catalog release tag. */ + @JsonProperty("releaseTag") String releaseTag, /** Current authoritative service connection state. */ @JsonProperty("status") ConnectorCatalogStatus status, /** Opaque stable runtime IDs currently projected into the session for this Connector. */ @JsonProperty("runtimeServerIds") List runtimeServerIds ) { + + /** + * Creates a record with the components it had before later optional fields were added. + * + * @param name Canonical Connector name used by lifecycle methods. + * @param displayName Untrusted display label from the service. + * @param description Untrusted service description, when present. + * @param status Current authoritative service connection state. + * @param runtimeServerIds Opaque stable runtime IDs currently projected into the session for this Connector. + */ + public ConnectorCatalogEntry( + String name, + String displayName, + String description, + ConnectorCatalogStatus status, + List runtimeServerIds + ) { + this(name, displayName, description, null, null, null, status, runtimeServerIds); + } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorSessionAccount.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorSessionAccount.java new file mode 100644 index 0000000000..fcf63dc7d4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectorSessionAccount.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Session account selection. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ConnectorSessionAccount( + /** Opaque session-scoped account selection ID. */ + @JsonProperty("accountId") String accountId, + /** Credential-free identity metadata. */ + @JsonProperty("authInfo") AuthIdentityMetadata authInfo +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java index bf5f9a8b97..3088672b69 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java @@ -23,6 +23,8 @@ public record CurrentModel( /** Currently active model identifier */ @JsonProperty("modelId") String modelId, + /** 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. */ + @JsonProperty("planBaseModelId") String planBaseModelId, /** 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. */ @JsonProperty("reasoningEffort") String reasoningEffort, /** Context tier for models that support multiple context-window sizes. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadOutcome.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadOutcome.java new file mode 100644 index 0000000000..d8941b4567 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadOutcome.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Result of one customization reload component. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CustomizationReloadOutcome( + /** Component whose reload was attempted or skipped */ + @JsonProperty("subsystem") CustomizationReloadSubsystem subsystem, + /** Whether the component reloaded, was skipped, or failed */ + @JsonProperty("status") CustomizationReloadStatus status, + /** Reason for a skipped component or description of a failure, when available */ + @JsonProperty("detail") String detail +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadStatus.java new file mode 100644 index 0000000000..b34528549b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadStatus.java @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Result of reloading a customization component. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class CustomizationReloadStatus { + /** The {@code reloaded} variant. */ + public static final CustomizationReloadStatus RELOADED = new CustomizationReloadStatus("reloaded"); + /** The {@code skipped} variant. */ + public static final CustomizationReloadStatus SKIPPED = new CustomizationReloadStatus("skipped"); + /** The {@code failed} variant. */ + public static final CustomizationReloadStatus FAILED = new CustomizationReloadStatus("failed"); + /** The default value when no recognized variant is available. */ + public static final CustomizationReloadStatus UNKNOWN = new CustomizationReloadStatus("unknown"); + + private final String value; + private CustomizationReloadStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static CustomizationReloadStatus fromValue(String value) { + if (RELOADED.value.equals(value)) return RELOADED; + if (SKIPPED.value.equals(value)) return SKIPPED; + if (FAILED.value.equals(value)) return FAILED; + if (UNKNOWN.value.equals(value)) return UNKNOWN; + if (value != null) return new CustomizationReloadStatus(value); + throw new IllegalArgumentException("Unknown CustomizationReloadStatus value: " + value); + } + @Override + public boolean equals(Object other) { + return other instanceof CustomizationReloadStatus that && value.equals(that.value); + } + @Override + public int hashCode() { return value.hashCode(); } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadSubsystem.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadSubsystem.java new file mode 100644 index 0000000000..989bde0a7c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationReloadSubsystem.java @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Component of session customization discovery. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class CustomizationReloadSubsystem { + /** The {@code repositoryContext} variant. */ + public static final CustomizationReloadSubsystem REPOSITORYCONTEXT = new CustomizationReloadSubsystem("repositoryContext"); + /** The {@code instructions} variant. */ + public static final CustomizationReloadSubsystem INSTRUCTIONS = new CustomizationReloadSubsystem("instructions"); + /** The {@code plugins} variant. */ + public static final CustomizationReloadSubsystem PLUGINS = new CustomizationReloadSubsystem("plugins"); + /** The {@code hooks} variant. */ + public static final CustomizationReloadSubsystem HOOKS = new CustomizationReloadSubsystem("hooks"); + /** The {@code skills} variant. */ + public static final CustomizationReloadSubsystem SKILLS = new CustomizationReloadSubsystem("skills"); + /** The {@code agents} variant. */ + public static final CustomizationReloadSubsystem AGENTS = new CustomizationReloadSubsystem("agents"); + /** The {@code mcp} variant. */ + public static final CustomizationReloadSubsystem MCP = new CustomizationReloadSubsystem("mcp"); + /** The {@code extensions} variant. */ + public static final CustomizationReloadSubsystem EXTENSIONS = new CustomizationReloadSubsystem("extensions"); + /** The default value when no recognized variant is available. */ + public static final CustomizationReloadSubsystem UNKNOWN = new CustomizationReloadSubsystem("unknown"); + + private final String value; + private CustomizationReloadSubsystem(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static CustomizationReloadSubsystem fromValue(String value) { + if (REPOSITORYCONTEXT.value.equals(value)) return REPOSITORYCONTEXT; + if (INSTRUCTIONS.value.equals(value)) return INSTRUCTIONS; + if (PLUGINS.value.equals(value)) return PLUGINS; + if (HOOKS.value.equals(value)) return HOOKS; + if (SKILLS.value.equals(value)) return SKILLS; + if (AGENTS.value.equals(value)) return AGENTS; + if (MCP.value.equals(value)) return MCP; + if (EXTENSIONS.value.equals(value)) return EXTENSIONS; + if (UNKNOWN.value.equals(value)) return UNKNOWN; + if (value != null) return new CustomizationReloadSubsystem(value); + throw new IllegalArgumentException("Unknown CustomizationReloadSubsystem value: " + value); + } + @Override + public boolean equals(Object other) { + return other instanceof CustomizationReloadSubsystem that && value.equals(that.value); + } + @Override + public int hashCode() { return value.hashCode(); } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationsReloadResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationsReloadResult.java new file mode 100644 index 0000000000..2f84a8ee4b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CustomizationsReloadResult.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CustomizationsReloadResult( + /** Warnings from skill discovery */ + @JsonProperty("warnings") List warnings, + /** Errors from any component that could not be refreshed */ + @JsonProperty("errors") List errors, + /** Outcome of each component in reload order; a skipped component was not configured or loaded */ + @JsonProperty("outcomes") List outcomes +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredModel.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredModel.java new file mode 100644 index 0000000000..c019f85165 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredModel.java @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.time.OffsetDateTime; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * 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. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record DiscoveredModel( + /** Provider-native model identifier. */ + @JsonProperty("id") String id, + /** Provider-reported display name. */ + @JsonProperty("name") String name, + /** Attribution for the adapter that produced this model row. */ + @JsonProperty("provenance") ModelProviderProvenance provenance, + /** Provider-reported artifact digest. */ + @JsonProperty("digest") String digest, + /** Provider-reported last-modified timestamp. */ + @JsonProperty("modifiedAt") OffsetDateTime modifiedAt, + /** Provider-reported artifact size in bytes. */ + @JsonProperty("sizeBytes") Long sizeBytes, + /** Provider-reported model artifact details. */ + @JsonProperty("details") ModelArtifactDetails details, + /** Provider-reported model capabilities. Omitted capability fields are unknown; explicit false values are preserved. */ + @JsonProperty("capabilities") ModelCapabilities capabilities, + /** Non-fatal warnings encountered while enriching this model. */ + @JsonProperty("warnings") List warnings +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResultOk.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResultOk.java deleted file mode 100644 index 84e4d65970..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResultOk.java +++ /dev/null @@ -1,51 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import javax.annotation.processing.Generated; - -/** - * Variant {@code ok} of {@link EntraTokenAcquireResult}. - * - * @since 1.0.0 - */ -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class EntraTokenAcquireResultOk extends EntraTokenAcquireResult { - - @JsonProperty("status") - private final String status = "ok"; - - @Override - public String getStatus() { return status; } - - /** Opaque access token. */ - @JsonProperty("accessToken") - private String accessToken; - - /** Expiry as milliseconds since Unix epoch, when supplied by OneAuth. */ - @JsonProperty("expiresOnTimestamp") - private Double expiresOnTimestamp; - - /** Opaque OneAuth account id, when supplied by the broker. */ - @JsonProperty("accountId") - private String accountId; - - public String getAccessToken() { return accessToken; } - public void setAccessToken(String accessToken) { this.accessToken = accessToken; } - - public Double getExpiresOnTimestamp() { return expiresOnTimestamp; } - public void setExpiresOnTimestamp(Double expiresOnTimestamp) { this.expiresOnTimestamp = expiresOnTimestamp; } - - public String getAccountId() { return accountId; } - public void setAccountId(String accountId) { this.accountId = accountId; } -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentCapabilities.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentCapabilities.java new file mode 100644 index 0000000000..f261c345db --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentCapabilities.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Hosting capabilities and session capacity advertised by an environment. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record EnvironmentCapabilities( + /** Advertised Agent Host Protocol version. */ + @JsonProperty("ahpVersion") String ahpVersion, + /** Feature identifiers advertised by the environment. */ + @JsonProperty("features") List features, + /** Maximum session capacity, when advertised. */ + @JsonProperty("maxSessions") Long maxSessions, + /** Current session count, when advertised. */ + @JsonProperty("currentSessions") Long currentSessions +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentKind.java new file mode 100644 index 0000000000..d7ae216cce --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentKind.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * GitHub Mission Control compute kind. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum EnvironmentKind { + /** The {@code user-local} variant. */ + USER_LOCAL("user-local"), + /** The {@code user-codespace} variant. */ + USER_CODESPACE("user-codespace"), + /** The {@code managed-actions} variant. */ + MANAGED_ACTIONS("managed-actions"), + /** The {@code managed-sandbox} variant. */ + MANAGED_SANDBOX("managed-sandbox"), + /** The {@code managed-cca} variant. */ + MANAGED_CCA("managed-cca"); + + private final String value; + EnvironmentKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static EnvironmentKind fromValue(String value) { + for (EnvironmentKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown EnvironmentKind value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostMaterializeSessionResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentsDeleteParams.java similarity index 78% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostMaterializeSessionResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentsDeleteParams.java index ea45e0bbbc..cfe9fe5da3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostMaterializeSessionResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentsDeleteParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * The resident session the application has materialized on its own connection. + * Identify a user-managed Mission Control environment to delete. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -23,8 +23,8 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record HostMaterializeSessionResult( - /** Runtime session UUID materialized on the application's SDK connection. */ - @JsonProperty("sessionId") String sessionId +public record EnvironmentsDeleteParams( + /** User-managed environment to delete. GitHub-managed environments cannot be deleted. */ + @JsonProperty("environmentId") String environmentId ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentsGetParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentsGetParams.java new file mode 100644 index 0000000000..c8c76fd744 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentsGetParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identify a Mission Control environment to retrieve. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record EnvironmentsGetParams( + /** Identifier assigned by Mission Control. */ + @JsonProperty("environmentId") String environmentId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentsGetResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentsGetResult.java new file mode 100644 index 0000000000..eeed1d9631 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentsGetResult.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Safe discovery information for the requested environment. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record EnvironmentsGetResult( + /** The requested environment without relay bootstrap credentials. */ + @JsonProperty("environment") GitHubEnvironment environment +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentsListParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentsListParams.java new file mode 100644 index 0000000000..b04a4ff1c5 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentsListParams.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Optional discovery filters supported by GitHub Mission Control. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record EnvironmentsListParams( + /** Restrict discovery to this compute kind. */ + @JsonProperty("kind") EnvironmentKind kind, + /** Operational status, such as online, offline, degraded, waking, or draining. */ + @JsonProperty("status") String status +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentsListResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentsListResult.java new file mode 100644 index 0000000000..23462fb88d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EnvironmentsListResult.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Environments visible to the authenticated caller and matching the supplied filters. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record EnvironmentsListResult( + /** Safe discovery records without relay bootstrap credentials. */ + @JsonProperty("environments") List environments +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubEnvironment.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubEnvironment.java new file mode 100644 index 0000000000..fffed71974 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/GitHubEnvironment.java @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Safe discovery information. Host-side relay bootstrap credentials are never included. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record GitHubEnvironment( + /** Identifier assigned by Mission Control. */ + @JsonProperty("id") String id, + /** Human-readable environment name. */ + @JsonProperty("name") String name, + /** Compute kind reported by Mission Control. */ + @JsonProperty("kind") EnvironmentKind kind, + /** Open-ended operational status vocabulary. */ + @JsonProperty("status") String status, + /** Hosting capabilities advertised by the environment. */ + @JsonProperty("capabilities") EnvironmentCapabilities capabilities, + /** Identifier of the environment owner. */ + @JsonProperty("ownerId") String ownerId, + /** Owner category reported by Mission Control. */ + @JsonProperty("ownerType") String ownerType, + /** Organization identifier, when the environment belongs to an organization. */ + @JsonProperty("orgId") String orgId, + /** Discovery labels attached to the environment. */ + @JsonProperty("labels") Map labels, + /** Timestamp of the last heartbeat received by Mission Control. */ + @JsonProperty("lastHeartbeatAt") String lastHeartbeatAt +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookInvokeRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookInvokeRequest.java index 9ed02d28b0..102b13f428 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookInvokeRequest.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookInvokeRequest.java @@ -20,7 +20,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record HookInvokeRequest( +record HookInvokeRequest( @JsonProperty("sessionId") String sessionId, @JsonProperty("hookType") HookType hookType, @JsonProperty("input") Object input diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResultInteractionRequired.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookInvokeResponse.java similarity index 70% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResultInteractionRequired.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookInvokeResponse.java index 96e635e027..7a799afcca 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenAcquireResultInteractionRequired.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookInvokeResponse.java @@ -13,18 +13,14 @@ import javax.annotation.processing.Generated; /** - * Variant {@code interaction-required} of {@link EntraTokenAcquireResult}. + * Optional output returned by an SDK callback hook. * * @since 1.0.0 */ -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) @javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class EntraTokenAcquireResultInteractionRequired extends EntraTokenAcquireResult { - - @JsonProperty("status") - private final String status = "interaction-required"; - - @Override - public String getStatus() { return status; } +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record HookInvokeResponse( + @JsonProperty("output") Object output +) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostConfiguration.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostConfiguration.java new file mode 100644 index 0000000000..a9b392a8d1 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostConfiguration.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Normalized listener settings delivered only to the supervised hosting participant. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record HostConfiguration( + /** Normalized local listener settings, absent for relay-only hosts. */ + @JsonProperty("localServer") HostLocalServerConfiguration localServer, + /** Requested GitHub Mission Control registration. */ + @JsonProperty("githubEnvironment") HostGitHubEnvironmentOptions gitHubEnvironment, + /** Whether session materialization is delegated to the owning application. */ + @JsonProperty("sessionFactory") Boolean sessionFactory, + /** Whether app-owned durable sessions are resumed by the owning application. */ + @JsonProperty("resumeFactory") Boolean resumeFactory +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostCreateSessionParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostCreateSessionParams.java index 0a3fbb1f5f..6c2233be78 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostCreateSessionParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostCreateSessionParams.java @@ -24,7 +24,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record HostCreateSessionParams( +record HostCreateSessionParams( /** Unique identity for this participation, independent of the session lifetime. */ @JsonProperty("handoffId") String handoffId, /** Resume an app-owned durable session instead of creating a new session. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostEnvironmentCredentials.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostEnvironmentCredentials.java new file mode 100644 index 0000000000..51ad33c23b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostEnvironmentCredentials.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Private credentials delivered only to a runtime-owned Mission Control hosting participant. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record HostEnvironmentCredentials( + /** Current bearer token for the authenticated GitHub identity. */ + @JsonProperty("token") String token, + /** Hostname of the authenticated GitHub service. */ + @JsonProperty("githubHost") String gitHubHost, + /** GitHub API base URL for the authenticated service. */ + @JsonProperty("githubApiUrl") String gitHubApiUrl, + /** Mission Control API origin for environment registration and management. */ + @JsonProperty("missionControlUrl") String missionControlUrl +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostGetConfigurationParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostGetConfigurationParams.java index 7eb71f2d37..2185af620e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostGetConfigurationParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostGetConfigurationParams.java @@ -23,5 +23,5 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record HostGetConfigurationParams() { +record HostGetConfigurationParams() { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostShutdownResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostGetEnvironmentCredentialsParams.java similarity index 95% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostShutdownResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostGetEnvironmentCredentialsParams.java index 46bb97b8d9..9b67713cba 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostShutdownResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostGetEnvironmentCredentialsParams.java @@ -23,5 +23,5 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record HostShutdownResult() { +record HostGetEnvironmentCredentialsParams() { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostGitHubEnvironmentOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostGitHubEnvironmentOptions.java new file mode 100644 index 0000000000..065a9f9328 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostGitHubEnvironmentOptions.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * GitHub Mission Control registration options. The compute ID is application-owned and stable. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record HostGitHubEnvironmentOptions( + /** Human-readable environment display name. */ + @JsonProperty("name") String name, + /** Stable application installation identity, reused across host restarts. */ + @JsonProperty("computeId") String computeId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostGetConfigurationResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostLocalServerConfiguration.java similarity index 67% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostGetConfigurationResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostLocalServerConfiguration.java index 529218ae47..86f544e4ff 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostGetConfigurationResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostLocalServerConfiguration.java @@ -10,20 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** - * Normalized listener settings delivered only to the supervised hosting participant. + * Normalized local WebSocket listener settings. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record HostGetConfigurationResult( +record HostLocalServerConfiguration( /** Hostname or IP address to bind. */ @JsonProperty("hostname") String hostname, /** Port to bind, with zero requesting OS allocation. */ @@ -31,10 +28,6 @@ public record HostGetConfigurationResult( /** Secret connection token, absent when authentication is disabled. */ @JsonProperty("token") String token, /** Whether the listener requires token authentication. */ - @JsonProperty("requireConnectionToken") Boolean requireConnectionToken, - /** Whether session materialization is delegated to the owning application. */ - @JsonProperty("sessionFactory") Boolean sessionFactory, - /** Whether app-owned durable sessions are resumed by the owning application. */ - @JsonProperty("resumeFactory") Boolean resumeFactory + @JsonProperty("requireConnectionToken") Boolean requireConnectionToken ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostLocalServerOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostLocalServerOptions.java new file mode 100644 index 0000000000..b85fe85722 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostLocalServerOptions.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Local WebSocket transport options. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record HostLocalServerOptions( + /** Listener hostname. Defaults to 127.0.0.1; explicit non-loopback binds are allowed. */ + @JsonProperty("hostname") String hostname, + /** Listener port. Omitted or zero requests an OS-allocated port. */ + @JsonProperty("port") Long port, + /** Nonempty connection token. Generated randomly when required and omitted. */ + @JsonProperty("token") String token, + /** Require token authentication (default true). Cannot be false with a token. */ + @JsonProperty("requireConnectionToken") Boolean requireConnectionToken +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostPublishSessionResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostPublishSessionResult.java index 9c276c70cd..3bad91e955 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostPublishSessionResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostPublishSessionResult.java @@ -10,16 +10,13 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * The existing runtime identity and its resource on the listener. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostReadyParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostReadyParams.java index db94081779..8d1ed23bb6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostReadyParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostReadyParams.java @@ -23,10 +23,12 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record HostReadyParams( +record HostReadyParams( /** Actual bound WebSocket URL. */ @JsonProperty("address") String address, /** Configured secret token, absent when authentication is disabled. */ - @JsonProperty("token") String token + @JsonProperty("token") String token, + /** Registered environment ID, reported only once the relay transport is connected. */ + @JsonProperty("environmentId") String environmentId ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostRegisterSessionRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostRegisterSessionRequest.java index d121dcad3e..f431893e28 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostRegisterSessionRequest.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostRegisterSessionRequest.java @@ -21,7 +21,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record HostRegisterSessionRequest( +record HostRegisterSessionRequest( /** Canonical ID of the existing resident runtime session. */ @JsonProperty("sessionId") String sessionId, /** Absolute working directory of the resident session. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostReleaseSessionParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostReleaseSessionParams.java index 26c69fdb2e..894687ca09 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostReleaseSessionParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostReleaseSessionParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record HostReleaseSessionParams( +record HostReleaseSessionParams( /** Identity of the participation to release without destroying the session. */ @JsonProperty("handoffId") String handoffId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostSessionCreateCallback.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostSessionCreateCallback.java index 7620ef03dc..f35ea3d1a6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostSessionCreateCallback.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostSessionCreateCallback.java @@ -21,7 +21,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record HostSessionCreateCallback( +record HostSessionCreateCallback( /** Listener UUID identifying the owning application's host. */ @JsonProperty("hostId") String hostId, /** Unique identity of the session participation being requested. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostCreateSessionResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostSessionCreateResult.java similarity index 84% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostCreateSessionResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostSessionCreateResult.java index 90827fd4e1..96f0c87ec4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostCreateSessionResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostSessionCreateResult.java @@ -10,20 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * The resident session the application has materialized on its own connection. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record HostCreateSessionResult( +record HostSessionCreateResult( /** Runtime session UUID materialized on the application's SDK connection. */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostSessionReleasedNotification.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostSessionReleasedNotification.java index d00385a364..6d06c6b9a6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostSessionReleasedNotification.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostSessionReleasedNotification.java @@ -20,7 +20,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record HostSessionReleasedNotification( +record HostSessionReleasedNotification( /** Listener UUID whose application session participation ended. */ @JsonProperty("hostId") String hostId, /** Identity of the handoff retaining the original application session object. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostStartParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostStartParams.java index 13c21e716a..b60a36451d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostStartParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostStartParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Starts a supervised AHP listener in the runtime's configured working directory. + * Starts a supervised AHP host with at least one explicitly selected transport. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -26,14 +26,10 @@ public record HostStartParams( /** Caller-generated UUID identifying this connection-owned listener. */ @JsonProperty("hostId") String hostId, - /** Listener hostname. Defaults to 127.0.0.1; explicit non-loopback binds are allowed. */ - @JsonProperty("hostname") String hostname, - /** Listener port. Omitted or zero requests an OS-allocated port. */ - @JsonProperty("port") Long port, - /** Nonempty connection token. Generated randomly when required and omitted. */ - @JsonProperty("token") String token, - /** Require token authentication (default true). Cannot be false with a token. */ - @JsonProperty("requireConnectionToken") Boolean requireConnectionToken, + /** Enables a local WebSocket listener. */ + @JsonProperty("localServer") HostLocalServerOptions localServer, + /** Registers a GitHub Mission Control environment and enables its relay transport. */ + @JsonProperty("githubEnvironment") HostGitHubEnvironmentOptions gitHubEnvironment, /** Ask the owning SDK application to materialize AHP sessions. */ @JsonProperty("sessionFactory") Boolean sessionFactory, /** Ask the owning application to resume its durable AHP sessions. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostStartResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostStartResult.java index 9073d3b7c1..4a9b908ecc 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostStartResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostStartResult.java @@ -30,6 +30,8 @@ public record HostStartResult( @JsonProperty("url") String url, /** Secret connection token, absent when authentication is disabled. */ @JsonProperty("token") String token, + /** GitHub Mission Control environment ID, present when its relay transport is ready. */ + @JsonProperty("environmentId") String environmentId, /** Separate host process ID, when provided by a legacy runtime. Absent for in-process listeners. */ @JsonProperty("pid") Long pid ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingMeta.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingMeta.java new file mode 100644 index 0000000000..46967ea441 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingMeta.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Lock state and provenance of one managed setting. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ManagedSettingMeta( + /** Whether users and repositories may choose a different value. `false` means policy locks the value. */ + @JsonProperty("overridable") Boolean overridable, + /** Channel that supplied this scalar value, matching a `layers[].source`: `device`, `server`, or `policyHelper`. These scalar defaults select one winning channel, not a mixed source. Treat unknown values as additional channels; more may be added. */ + @JsonProperty("source") String source +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenInteraction.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsChannel.java similarity index 59% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenInteraction.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsChannel.java index 4c83c4452e..1ab915c122 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/EntraTokenInteraction.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsChannel.java @@ -11,7 +11,7 @@ import javax.annotation.processing.Generated; /** - * How far OneAuth may go to acquire the requested token. + * A channel accepted by managedSettings.compose. * * @apiNote This type is experimental and may change in a future version. * @@ -19,23 +19,23 @@ */ @CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum EntraTokenInteraction { - /** The {@code silent} variant. */ - SILENT("silent"), - /** The {@code interactive} variant. */ - INTERACTIVE("interactive"), - /** The {@code force-interactive} variant. */ - FORCE_INTERACTIVE("force-interactive"); +public enum ManagedSettingsChannel { + /** The {@code device} variant. */ + DEVICE("device"), + /** The {@code server} variant. */ + SERVER("server"), + /** The {@code policyHelper} variant. */ + POLICYHELPER("policyHelper"); private final String value; - EntraTokenInteraction(String value) { this.value = value; } + ManagedSettingsChannel(String value) { this.value = value; } @com.fasterxml.jackson.annotation.JsonValue public String getValue() { return value; } @com.fasterxml.jackson.annotation.JsonCreator - public static EntraTokenInteraction fromValue(String value) { - for (EntraTokenInteraction v : values()) { + public static ManagedSettingsChannel fromValue(String value) { + for (ManagedSettingsChannel v : values()) { if (v.value.equals(value)) return v; } - throw new IllegalArgumentException("Unknown EntraTokenInteraction value: " + value); + throw new IllegalArgumentException("Unknown ManagedSettingsChannel value: " + value); } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddAtResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsComposeLayer.java similarity index 63% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddAtResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsComposeLayer.java index 7952fdc88b..b6267f167b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddAtResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsComposeLayer.java @@ -14,19 +14,20 @@ import javax.annotation.processing.Generated; /** - * Result of registering or re-arming a scheduled prompt. + * One candidate channel; absent settings represents a channel that delivered no document. + * + * @apiNote This type is experimental and may change in a future version. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionScheduleAddAtResult( - /** The registered or updated schedule entry. */ - @JsonProperty("entry") ScheduleEntry entry, - /** User-facing validation error, when registration failed. */ - @JsonProperty("error") String error +public record ManagedSettingsComposeLayer( + /** The channel whose candidate document is being supplied. */ + @JsonProperty("source") ManagedSettingsChannel source, + /** Candidate managed-settings document. Omit when the channel delivered none, as in resolve output. */ + @JsonProperty("settings") Object settings ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsComposeParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsComposeParams.java new file mode 100644 index 0000000000..ca313ce283 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsComposeParams.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Candidate managed-settings documents to merge without applying them. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ManagedSettingsComposeParams( + /** One entry per channel. `source` must be `device`, `server`, or `policyHelper`, each at most once (checked at runtime); order does not matter, because channel precedence is fixed. To preview documents from resolve output, map recognized source strings to ManagedSettingsChannel and copy their settings; generated resolve and compose layer types are distinct. Omitted settings means this channel delivered no document. Supplied documents must be valid within the preview limits; warnings are returned in diagnostics. Compose does not reproduce source-failure state or retained enforcement floors from resolve. */ + @JsonProperty("layers") List layers +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsComposeResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsComposeResult.java new file mode 100644 index 0000000000..37f3568360 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsComposeResult.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * The effective managed settings the runtime would enforce for the given documents, in the same shape `managedSettings.resolve` returns. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ManagedSettingsComposeResult( + /** Effective managed settings, in the same shape as `session.managedSettings.get`. */ + @JsonProperty("resolved") ManagedSettingsResolvedData resolved, + /** Typed effective values, as in `managedSettings.resolve`. */ + @JsonProperty("values") ManagedSettingsValues values, + /** Per-key lock state and provenance for `values`. */ + @JsonProperty("meta") ManagedSettingsMeta meta, + /** Only the supplied channels, strongest first, with canonical documents. Empty canonical documents are represented as absent settings, as in live resolution. */ + @JsonProperty("layers") List layers, + /** Warnings about ignored content, with paths prefixed by the channel name. */ + @JsonProperty("diagnostics") List diagnostics +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsDiagnostic.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsDiagnostic.java new file mode 100644 index 0000000000..22068ba231 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsDiagnostic.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * One validation finding for a managed-settings document. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ManagedSettingsDiagnostic( + /** Dot-separated path of the offending setting, such as `autoTier.overridable`. Empty for the document as a whole. */ + @JsonProperty("path") String path, + /** Whether the finding rejects the document. */ + @JsonProperty("severity") ManagedSettingsDiagnosticSeverity severity, + /** Human-readable description of the finding. */ + @JsonProperty("message") String message +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsDiagnosticSeverity.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsDiagnosticSeverity.java new file mode 100644 index 0000000000..f5010d41b9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsDiagnosticSeverity.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Severity of a managed-settings validation finding. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ManagedSettingsDiagnosticSeverity { + /** The {@code error} variant. */ + ERROR("error"), + /** The {@code warning} variant. */ + WARNING("warning"); + + private final String value; + ManagedSettingsDiagnosticSeverity(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ManagedSettingsDiagnosticSeverity fromValue(String value) { + for (ManagedSettingsDiagnosticSeverity v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ManagedSettingsDiagnosticSeverity value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsLayer.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsLayer.java new file mode 100644 index 0000000000..07b915e858 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsLayer.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * One managed-settings channel and the document it delivered. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ManagedSettingsLayer( + /** Channel identifier: `device` (MDM, plist, registry, or managed file), `server` (account or organization policy), or `policyHelper` (session-local helper output, supported by compose). Treat unknown output values as additional channels; more may be added. */ + @JsonProperty("source") String source, + /** Validated managed-settings document this channel delivered. Absent when the channel delivered none. */ + @JsonProperty("settings") Object settings +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsMeta.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsMeta.java new file mode 100644 index 0000000000..7348141749 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsMeta.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Per-key lock state and provenance for `ManagedSettingsValues`, with the same field names. Producers emit each typed key in values and meta together; both outer objects are omitted when no typed key is set. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ManagedSettingsMeta( + /** Lock state and provenance of `values.model`. */ + @JsonProperty("model") ManagedSettingMeta model, + /** Lock state and provenance of `values.autoTier`. */ + @JsonProperty("autoTier") ManagedSettingMeta autoTier +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsResolveParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsResolveParams.java new file mode 100644 index 0000000000..ffb5af2b18 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsResolveParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Request parameters for the {@code managedSettings.resolve} RPC method. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ManagedSettingsResolveParams( + /** Opaque account identifier returned by `account.getAllUsers`. When omitted, the current account is used, or device policy only when no account is signed in. */ + @JsonProperty("selectionId") String selectionId, + /** GitHub token to resolve instead of the current account. The call fails when the token cannot be resolved. */ + @JsonProperty("gitHubToken") String gitHubToken, + /** Embedding client identity for server policy requests, as in session creation. Omit for the CLI identity. */ + @JsonProperty("clientName") String clientName +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsResolveResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsResolveResult.java new file mode 100644 index 0000000000..6d9941f4a9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsResolveResult.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Effective enterprise managed settings for an account, resolved without a session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ManagedSettingsResolveResult( + /** Printable opaque identity of the account the settings were resolved for, suitable for comparison and storage, not an account selectionId. Absent when no account was available, in which case only device policy is reported. */ + @JsonProperty("account") String account, + /** Effective managed settings from the device and account (server) channels, in the same shape as `session.managedSettings.get`, excluding session-local injection. */ + @JsonProperty("resolved") ManagedSettingsResolvedData resolved, + /** Typed effective values of managed settings, keyed like the managed-settings schema and already resolved across channels, with the `{ "overridable": ... }` wrapper removed. Present when policy sets at least one typed key. Keys not typed here are available in `resolved.settings`. */ + @JsonProperty("values") ManagedSettingsValues values, + /** Per-key lock state and provenance for the entries in `values`, using the same key names. */ + @JsonProperty("meta") ManagedSettingsMeta meta, + /** Each managed-settings channel consulted, strongest first, with the validated document it delivered before merging. `resolved.settings` is the merged result. More channels may be added over time. */ + @JsonProperty("layers") List layers, + /** Warnings about unavailable policy sources or a failed refresh served from cache. A cached response is not proof of a successful live fetch; `resolved.failClosed` separately describes enforcement. */ + @JsonProperty("diagnostics") List diagnostics +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsResolvedData.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsResolvedData.java new file mode 100644 index 0000000000..2020e9d434 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsResolvedData.java @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Effective enterprise managed settings and contributing channels. Session events report applied policy; sessionless resolve reports an account/device snapshot, and compose reports a non-applying preview of candidate documents. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively. Session-local SDK-client policy is included only in session results. Marked experimental while the managed-settings surface stabilizes. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ManagedSettingsResolvedData( + /** Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. */ + @JsonProperty("source") ManagedSettingsResolvedSource source, + /** Whether the server (account/org) managed-settings layer was present */ + @JsonProperty("serverManaged") Boolean serverManaged, + /** Whether an actual device MDM/plist/registry/file managed-settings layer was present */ + @JsonProperty("deviceManaged") Boolean deviceManaged, + /** Whether a session-local permissions layer injected by the SDK host was present */ + @JsonProperty("clientManaged") Boolean clientManaged, + /** Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. */ + @JsonProperty("policyHelperManaged") Boolean policyHelperManaged, + /** Whether managed policy could not be determined (e.g. a failed server fetch) and the session fell back to the fail-closed restriction. When true, restrictions such as disabling bypass-permissions are enforced even though `settings` may be absent. */ + @JsonProperty("failClosed") Boolean failClosed, + /** Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. */ + @JsonProperty("sandboxEnabledByUndeterminedPolicy") Boolean sandboxEnabledByUndeterminedPolicy, + /** Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true. */ + @JsonProperty("bypassPermissionsDisabled") Boolean bypassPermissionsDisabled, + /** Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. */ + @JsonProperty("permissionsAllowIntersected") Boolean permissionsAllowIntersected, + /** The setting keys under enterprise management in the effective managed settings (e.g. `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force. */ + @JsonProperty("managedKeys") List managedKeys, + /** The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. */ + @JsonProperty("settings") Object settings +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsSchemaResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsSchemaResult.java new file mode 100644 index 0000000000..0975e8f4ff --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsSchemaResult.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * The authoring JSON schema for managed settings recognized by this runtime. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ManagedSettingsSchemaResult( + /** JSON schema (draft 2020-12) with descriptive shared `x-composition` annotations, not a complete runtime composition contract. Model, effortLevel, and contextTier remain coupled; use `managedSettings.compose` for the runtime's effective result. */ + @JsonProperty("schema") Object schema, + /** Version of the runtime that owns this schema. */ + @JsonProperty("runtimeVersion") String runtimeVersion +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsValidateParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsValidateParams.java new file mode 100644 index 0000000000..32879f689c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsValidateParams.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * A candidate managed-settings document to validate without applying it. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ManagedSettingsValidateParams( + /** The document to validate: a JSON object, or a string containing the document's JSON text. Preview documents are limited to 1 MiB and 64 levels of nesting, a stricter resource limit than delivered-policy parsing; violations are returned as diagnostics. */ + @JsonProperty("content") Object content, + /** Channel the document is meant for (`device`, `server`, or `policyHelper`). Some keys are only honored in some channels; for example, a `policyHelper` registration is ignored in policy-helper output. When omitted, no channel-specific checks run. */ + @JsonProperty("layer") String layer +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsValidateResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsValidateResult.java new file mode 100644 index 0000000000..ffc3da6340 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsValidateResult.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Result of validating a managed-settings document. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ManagedSettingsValidateResult( + /** Whether the runtime would accept the document within the preview resource limits. Always equals whether `settings` is present. An invalid document is rejected as a whole. */ + @JsonProperty("valid") Boolean valid, + /** Canonical form of the document the runtime would apply, with unrecognized keys removed. Absent when the document is invalid. */ + @JsonProperty("settings") Object settings, + /** Errors that reject the document and warnings about content the runtime ignores. */ + @JsonProperty("diagnostics") List diagnostics +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsValues.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsValues.java new file mode 100644 index 0000000000..931294b19c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ManagedSettingsValues.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Typed effective values of managed settings. Each field mirrors the managed-settings schema key of the same name; more keys are added as they are typed. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ManagedSettingsValues( + /** Managed default model identifier, as configured. New sessions start with it; it can name a model the account cannot use, so hosts match it against the listed models. */ + @JsonProperty("model") String model, + /** Managed Auto routing preference, used when the selected model is `auto`. */ + @JsonProperty("autoTier") AutoTier autoTier +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpAllowedServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpAllowedServer.java index 1d0a17cc9b..165559f7f8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpAllowedServer.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpAllowedServer.java @@ -20,7 +20,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record McpAllowedServer( +record McpAllowedServer( /** Allowed server name */ @JsonProperty("name") String name, /** PII-free note explaining why the server was allowed */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfigureGitHubResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfigureGitHubResult.java new file mode 100644 index 0000000000..bebf76808f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfigureGitHubResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Result of configuring GitHub MCP. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record McpConfigureGitHubResult( + /** Whether GitHub MCP configuration changed. */ + @JsonProperty("changed") Boolean changed +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServer.java new file mode 100644 index 0000000000..4e37d915f8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServer.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Effective MCP configuration entry. Configuration enablement is distinct from the optional live observation. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpConfiguredServer( + /** Server name (config key) */ + @JsonProperty("name") String name, + /** Whether this configured server is enabled after session configuration and policy filtering. */ + @JsonProperty("enabled") Boolean enabled, + /** Configuration provenance: user, workspace, plugin, builtin, or managed. */ + @JsonProperty("source") McpServerSource source, + /** Plugin name that provided this server, when source is plugin. */ + @JsonProperty("sourcePlugin") String sourcePlugin, + /** Plugin version that provided this server, when source is plugin. */ + @JsonProperty("sourcePluginVersion") String sourcePluginVersion, + /** Human-readable display name supplied by configuration. */ + @JsonProperty("displayName") String displayName, + /** Observed state from an already materialized matching server. Omitted when no live graph has this configured server; it never determines configuration enablement. */ + @JsonProperty("live") McpConfiguredServerState live +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddSelfPacedResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServerState.java similarity index 68% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddSelfPacedResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServerState.java index 65f8745baf..0087f2546b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddSelfPacedResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfiguredServerState.java @@ -14,19 +14,20 @@ import javax.annotation.processing.Generated; /** - * Result of registering or re-arming a scheduled prompt. + * Observational state for a matching already materialized MCP server. + * + * @apiNote This type is experimental and may change in a future version. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionScheduleAddSelfPacedResult( - /** The registered or updated schedule entry. */ - @JsonProperty("entry") ScheduleEntry entry, - /** User-facing validation error, when registration failed. */ +public record McpConfiguredServerState( + /** Observed connection status. This is not a configuration or readiness guarantee. */ + @JsonProperty("status") McpServerStatus status, + /** Observed connection error, when the materialized server failed. */ @JsonProperty("error") String error ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpFailedServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpFailedServer.java index 36009e4915..13ea4c6135 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpFailedServer.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpFailedServer.java @@ -20,7 +20,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record McpFailedServer( +record McpFailedServer( /** The config key of the server that failed to connect. */ @JsonProperty("name") String name, /** The captured connection failure detail. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpFilteredServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpFilteredServer.java index e0ecefae76..60f62325ae 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpFilteredServer.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpFilteredServer.java @@ -20,7 +20,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record McpFilteredServer( +record McpFilteredServer( /** Filtered server name */ @JsonProperty("name") String name, /** Human-readable filter reason */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPrompt.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPrompt.java new file mode 100644 index 0000000000..b6458bb582 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPrompt.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * An MCP prompt descriptor. Server-provided non-standard fields are exposed under `additionalProperties`. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpPrompt( + /** The programmatic name of the prompt */ + @JsonProperty("name") String name, + /** Human-readable display title */ + @JsonProperty("title") String title, + /** Description of what this prompt provides */ + @JsonProperty("description") String description, + /** Arguments accepted by the prompt */ + @JsonProperty("arguments") List arguments, + /** Icons associated with this prompt */ + @JsonProperty("icons") List icons, + /** Prompt-level metadata */ + @JsonProperty("_meta") Map meta, + /** Server-provided non-standard descriptor fields */ + @JsonProperty("additionalProperties") Map additionalProperties +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPromptArgument.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPromptArgument.java new file mode 100644 index 0000000000..75ce67bbb8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPromptArgument.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * An argument accepted by an MCP prompt. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpPromptArgument( + /** Name of the argument */ + @JsonProperty("name") String name, + /** Description of the argument */ + @JsonProperty("description") String description, + /** Whether the argument is required; omission is distinct from false */ + @JsonProperty("required") Boolean required, + /** Argument-level metadata */ + @JsonProperty("_meta") Map meta, + /** Server-provided non-standard argument fields */ + @JsonProperty("additionalProperties") Map additionalProperties +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPromptIcon.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPromptIcon.java new file mode 100644 index 0000000000..0c4b7f0d4e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPromptIcon.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * An MCP prompt icon with standard size hints and preserved non-standard fields. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpPromptIcon( + /** Icon URI */ + @JsonProperty("src") String src, + /** Icon MIME type, when known */ + @JsonProperty("mimeType") String mimeType, + /** Icon sizes, such as `48x48` or `any` */ + @JsonProperty("sizes") List sizes, + /** Theme hint for this icon */ + @JsonProperty("theme") String theme, + /** Server-provided non-standard icon fields */ + @JsonProperty("additionalProperties") Map additionalProperties +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPromptMessage.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPromptMessage.java new file mode 100644 index 0000000000..785d050e93 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPromptMessage.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * An MCP prompt message with opaque JSON content preserved without flattening or content-type filtering. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpPromptMessage( + /** The role of the message sender */ + @JsonProperty("role") McpPromptRole role, + /** The original MCP content block, including nested metadata and unfamiliar content types */ + @JsonProperty("content") Object content, + /** Message-level metadata */ + @JsonProperty("_meta") Map meta, + /** Server-provided non-standard message fields */ + @JsonProperty("additionalProperties") Map additionalProperties +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPromptRole.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPromptRole.java new file mode 100644 index 0000000000..461ebb3b73 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpPromptRole.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * The sender role of an MCP prompt message. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpPromptRole { + /** The {@code user} variant. */ + USER("user"), + /** The {@code assistant} variant. */ + ASSISTANT("assistant"); + + private final String value; + McpPromptRole(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpPromptRole fromValue(String value) { + for (McpPromptRole v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpPromptRole value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpReloadWithConfigResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpStartServersResult.java similarity index 86% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpReloadWithConfigResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpStartServersResult.java index eba9a21c11..745eaa6d04 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpReloadWithConfigResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpStartServersResult.java @@ -10,21 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; /** * MCP server startup filtering result. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionMcpReloadWithConfigResult( +record McpStartServersResult( /** Servers filtered out before startup */ @JsonProperty("filteredServers") List filteredServers, /** Non-default servers allowed by policy */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelArtifactDetails.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelArtifactDetails.java new file mode 100644 index 0000000000..cef8cb97cc --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelArtifactDetails.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Provider-reported model artifact metadata. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelArtifactDetails( + /** Artifact format, such as `gguf`. */ + @JsonProperty("format") String format, + /** Primary model family. */ + @JsonProperty("family") String family, + /** Provider-reported model families. */ + @JsonProperty("families") List families, + /** Provider-reported parameter count label. */ + @JsonProperty("parameterSize") String parameterSize, + /** Provider-reported quantization label. */ + @JsonProperty("quantization") String quantization, + /** Provider-reported model architecture. */ + @JsonProperty("architecture") String architecture, + /** Provider-reported tokenizer. */ + @JsonProperty("tokenizer") String tokenizer +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelCapabilitiesSupports.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelCapabilitiesSupports.java index daaef4fcb4..17b9732e4d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelCapabilitiesSupports.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelCapabilitiesSupports.java @@ -21,6 +21,8 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record ModelCapabilitiesSupports( + /** Whether the model supports provider-native thinking. Independent of configurable reasoning effort; omission means unknown. */ + @JsonProperty("thinking") Boolean thinking, /** Whether this model supports vision/image input */ @JsonProperty("vision") Boolean vision, /** Whether this model supports canonical tool calling */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAdapterDescriptor.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAdapterDescriptor.java new file mode 100644 index 0000000000..1491a834d3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAdapterDescriptor.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * A normalized model-provider adapter in the session's effective catalog. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderAdapterDescriptor( + /** Stable opaque identity for routing to this adapter. Unique in the effective catalog, independent of live registration generations. */ + @JsonProperty("adapterId") String adapterId, + /** Descriptive provider family, such as `ollama`. Different adapters may have the same family; use adapterId for routing. */ + @JsonProperty("providerKind") String providerKind, + /** Human-readable provider name. */ + @JsonProperty("displayName") String displayName, + /** Attribution for the adapter itself. */ + @JsonProperty("provenance") ModelProviderAttribution provenance, + /** Adapter-declared policy for passive and automatic discovery. */ + @JsonProperty("automaticDiscovery") ModelProviderAutomaticDiscoveryPolicy automaticDiscovery, + /** Operations supported by this provider adapter. */ + @JsonProperty("operations") List operations +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAdapterOperationDescriptor.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAdapterOperationDescriptor.java new file mode 100644 index 0000000000..b859352e1b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAdapterOperationDescriptor.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * An operation supported by a model-provider adapter. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderAdapterOperationDescriptor( + /** Supported operation name: `discover`, `getStatus`, or `models.list`. Unknown names and duplicate declarations are rejected. */ + @JsonProperty("name") String name, + /** 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. */ + @JsonProperty("inputSchema") Object inputSchema +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAttribution.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAttribution.java new file mode 100644 index 0000000000..74072a9f16 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAttribution.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Contributor attribution, independent of routing identity and authorization. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderAttribution( + /** Kind of component that supplied the adapter. Attribution does not confer authority. */ + @JsonProperty("source") ModelProviderProvenanceSource source, + /** Stable contributor identifier. Required and nonblank for extension and custom sources; optional for built-in and configured sources. Does not grant authority. */ + @JsonProperty("ownerId") String ownerId, + /** Human-readable contributor name, not the adapter display name. */ + @JsonProperty("ownerDisplayName") String ownerDisplayName +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAutomaticDiscoveryMode.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAutomaticDiscoveryMode.java new file mode 100644 index 0000000000..1907b5a2a2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAutomaticDiscoveryMode.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * When the runtime may run an adapter without an explicit user action. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelProviderAutomaticDiscoveryMode { + /** The {@code automatic} variant. */ + AUTOMATIC("automatic"), + /** The {@code configuredOnly} variant. */ + CONFIGUREDONLY("configuredOnly"), + /** The {@code explicit} variant. */ + EXPLICIT("explicit"); + + private final String value; + ModelProviderAutomaticDiscoveryMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelProviderAutomaticDiscoveryMode fromValue(String value) { + for (ModelProviderAutomaticDiscoveryMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelProviderAutomaticDiscoveryMode value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAutomaticDiscoveryPolicy.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAutomaticDiscoveryPolicy.java new file mode 100644 index 0000000000..f7fd039063 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderAutomaticDiscoveryPolicy.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Adapter-declared policy that tells clients whether discovery may run automatically. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderAutomaticDiscoveryPolicy( + /** Whether automatic discovery is allowed, limited to configured providers, or explicit-only. */ + @JsonProperty("mode") ModelProviderAutomaticDiscoveryMode mode, + /** Maximum network scope used by this adapter during discovery. */ + @JsonProperty("networkScope") ModelProviderDiscoveryNetworkScope 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. */ + @JsonProperty("requiresInput") Boolean requiresInput, + /** True when the adapter must be enabled by a trusted owner, such as a trusted extension, before automatic discovery may run. */ + @JsonProperty("requiresTrust") Boolean requiresTrust +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderConfigurationDisposition.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderConfigurationDisposition.java new file mode 100644 index 0000000000..4c8b078fc4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderConfigurationDisposition.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Whether a planned configuration entry is new or already present in the session registry. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelProviderConfigurationDisposition { + /** The {@code create} variant. */ + CREATE("create"), + /** The {@code alreadyConfigured} variant. */ + ALREADYCONFIGURED("alreadyConfigured"); + + private final String value; + ModelProviderConfigurationDisposition(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelProviderConfigurationDisposition fromValue(String value) { + for (ModelProviderConfigurationDisposition v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelProviderConfigurationDisposition value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderDiscoveryNetworkScope.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderDiscoveryNetworkScope.java new file mode 100644 index 0000000000..30d889ee9b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderDiscoveryNetworkScope.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Network reach an adapter may use during discovery. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelProviderDiscoveryNetworkScope { + /** The {@code none} variant. */ + NONE("none"), + /** The {@code loopbackOnly} variant. */ + LOOPBACKONLY("loopbackOnly"), + /** The {@code configuredEndpointOnly} variant. */ + CONFIGUREDENDPOINTONLY("configuredEndpointOnly"), + /** The {@code localNetwork} variant. */ + LOCALNETWORK("localNetwork"), + /** The {@code internet} variant. */ + INTERNET("internet"); + + private final String value; + ModelProviderDiscoveryNetworkScope(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelProviderDiscoveryNetworkScope fromValue(String value) { + for (ModelProviderDiscoveryNetworkScope v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelProviderDiscoveryNetworkScope value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderInstance.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderInstance.java new file mode 100644 index 0000000000..06db8b4f3d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderInstance.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * A normalized model-provider instance discovered by the runtime. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderInstance( + /** Self-contained reference for subsequent provider operations. */ + @JsonProperty("reference") ModelProviderInstanceReference reference, + /** Human-readable instance name. */ + @JsonProperty("displayName") String displayName, + /** Attribution for the adapter that produced this instance. */ + @JsonProperty("provenance") ModelProviderProvenance provenance, + /** Inference API endpoint when the provider exposes one separately from its management endpoint. */ + @JsonProperty("inferenceEndpoint") String inferenceEndpoint, + /** Provider family to use for inference against this instance. */ + @JsonProperty("inferenceType") ProviderEndpointType inferenceType, + /** Wire API to use for inference against this instance, when required by the provider family. */ + @JsonProperty("inferenceWireApi") ProviderEndpointWireApi inferenceWireApi, + /** Transport to use for inference against this instance. */ + @JsonProperty("inferenceTransport") ProviderEndpointTransport inferenceTransport +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderInstanceReference.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderInstanceReference.java new file mode 100644 index 0000000000..621c26af9d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderInstanceReference.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Serializable reference to a discovered provider instance. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderInstanceReference( + /** Stable opaque identity of the adapter that owns this reference. Must be present in the target session's effective catalog. */ + @JsonProperty("adapterId") String adapterId, + /** Descriptive provider family. Must match the selected adapter; not a routing key. */ + @JsonProperty("providerKind") String providerKind, + /** Stable instance identifier derived by the provider adapter, such as `ollama:{normalizedEndpoint}`. */ + @JsonProperty("id") String 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. */ + @JsonProperty("managementEndpoint") String managementEndpoint +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderOperationOutcome.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderOperationOutcome.java new file mode 100644 index 0000000000..6e10eddd97 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderOperationOutcome.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Typed provider-operation outcome. Use the code for control flow and the optional message for display. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderOperationOutcome( + /** Machine-readable operation outcome. */ + @JsonProperty("code") ModelProviderOperationOutcomeCode code, + /** Human-readable detail for non-success outcomes. */ + @JsonProperty("message") String message +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderOperationOutcomeCode.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderOperationOutcomeCode.java new file mode 100644 index 0000000000..feab7ef5b8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderOperationOutcomeCode.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Typed outcome for a provider operation. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelProviderOperationOutcomeCode { + /** The {@code success} variant. */ + SUCCESS("success"), + /** The {@code absent} variant. */ + ABSENT("absent"), + /** The {@code unreachable} variant. */ + UNREACHABLE("unreachable"), + /** The {@code failed} variant. */ + FAILED("failed"); + + private final String value; + ModelProviderOperationOutcomeCode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelProviderOperationOutcomeCode fromValue(String value) { + for (ModelProviderOperationOutcomeCode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelProviderOperationOutcomeCode value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderProvenance.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderProvenance.java new file mode 100644 index 0000000000..186a32c91f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderProvenance.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Attribution for the adapter that produced a provider row. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderProvenance( + /** Stable opaque adapter identity from the effective catalog. Treat this as a whole identifier, not a parseable owner or kind. */ + @JsonProperty("adapterId") String adapterId, + /** Descriptive provider family that produced this row; not a routing key. */ + @JsonProperty("providerKind") String providerKind, + /** Kind of component that supplied the adapter. */ + @JsonProperty("source") ModelProviderProvenanceSource source, + /** Stable contributor identifier when the adapter has an owner outside the runtime. Independent of the contribution mechanism and not a routing key. */ + @JsonProperty("ownerId") String ownerId, + /** Human-readable contributor name, not the adapter display name. */ + @JsonProperty("ownerDisplayName") String ownerDisplayName +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderProvenanceSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderProvenanceSource.java new file mode 100644 index 0000000000..84e91393b9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderProvenanceSource.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Kind of component that supplied a provider adapter or row. Attribution does not confer authority. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelProviderProvenanceSource { + /** The {@code builtIn} variant. */ + BUILTIN("builtIn"), + /** The {@code configured} variant. */ + CONFIGURED("configured"), + /** The {@code extension} variant. */ + EXTENSION("extension"), + /** The {@code custom} variant. */ + CUSTOM("custom"); + + private final String value; + ModelProviderProvenanceSource(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelProviderProvenanceSource fromValue(String value) { + for (ModelProviderProvenanceSource v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelProviderProvenanceSource value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderWarning.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderWarning.java new file mode 100644 index 0000000000..deb6f08c78 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelProviderWarning.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * A non-fatal provider observation warning. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ModelProviderWarning( + /** Machine-readable warning code. */ + @JsonProperty("code") String code, + /** Human-readable warning message. */ + @JsonProperty("message") String message +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelSwitchToResult.java similarity index 92% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelSwitchToResult.java index 325b70c8f1..accae12e73 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelSwitchToResult.java @@ -10,21 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; /** * The model identifier active on the session after the switch. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionModelApplyStartupOverlayResult( +public record ModelSwitchToResult( /** Stable queue item identifier when this request was enqueued. Remains present if the item drains before the response is returned. */ @JsonProperty("queueId") String queueId, /** Currently active model identifier after the switch */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/NamedProviderConfig.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/NamedProviderConfig.java index dc57bca4f3..acb508a0d5 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/NamedProviderConfig.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/NamedProviderConfig.java @@ -40,6 +40,8 @@ public record NamedProviderConfig( @JsonProperty("azure") ProviderConfigAzure azure, /** Additional HTTP headers included with provider requests. */ @JsonProperty("headers") Map headers, + /** The product serving the provider's models, reported in telemetry as `model_provider`. Only affects telemetry. */ + @JsonProperty("modelProvider") ProviderConfigModelProvider modelProvider, /** Whether the host supplies bearer tokens dynamically. */ @JsonProperty("hasBearerTokenProvider") Boolean hasBearerTokenProvider ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderConfig.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderConfig.java index 22c7414996..4a3c2f7e8b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderConfig.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderConfig.java @@ -46,6 +46,8 @@ public record ProviderConfig( @JsonProperty("modelCapabilities") ModelCapabilitiesOverride modelCapabilities, /** Provider name used for model and telemetry attribution. */ @JsonProperty("providerName") String providerName, + /** 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. */ + @JsonProperty("modelProvider") ProviderConfigModelProvider modelProvider, /** The model identifier sent to the provider API for inference (the "wire" model), as opposed to modelId which is the well-known base. */ @JsonProperty("wireModel") String wireModel, /** Maximum prompt/input tokens for the model. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderConfigModelProvider.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderConfigModelProvider.java new file mode 100644 index 0000000000..62e1d74944 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ProviderConfigModelProvider.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * The product serving the model, reported in telemetry as `model_provider`. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ProviderConfigModelProvider { + /** The {@code openai} variant. */ + OPENAI("openai"), + /** The {@code anthropic} variant. */ + ANTHROPIC("anthropic"), + /** The {@code azure_openai} variant. */ + AZURE_OPENAI("azure_openai"), + /** The {@code ollama} variant. */ + OLLAMA("ollama"), + /** The {@code lm_studio} variant. */ + LM_STUDIO("lm_studio"), + /** The {@code foundry_local} variant. */ + FOUNDRY_LOCAL("foundry_local"), + /** The {@code llama_cpp} variant. */ + LLAMA_CPP("llama_cpp"); + + private final String value; + ProviderConfigModelProvider(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ProviderConfigModelProvider fromValue(String value) { + for (ProviderConfigModelProvider v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ProviderConfigModelProvider value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueBeginDeferredIdleDrainResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueBeginDeferredIdleDrainResult.java similarity index 88% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueBeginDeferredIdleDrainResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueBeginDeferredIdleDrainResult.java index 77e6decb9b..a90a855420 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueBeginDeferredIdleDrainResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueBeginDeferredIdleDrainResult.java @@ -16,14 +16,15 @@ /** * Whether a deferred-idle drain should run. * - * @apiNote This method is experimental and may change in a future version. + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ @CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionQueueBeginDeferredIdleDrainResult( +record QueueBeginDeferredIdleDrainResult( /** True when the host should run finishDeferredIdleDrain asynchronously. */ @JsonProperty("shouldDrain") Boolean shouldDrain ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueEnqueueResumePendingResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueEnqueueResumePendingResult.java similarity index 82% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueEnqueueResumePendingResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueEnqueueResumePendingResult.java index 324765b4a2..74e30209fd 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueEnqueueResumePendingResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueEnqueueResumePendingResult.java @@ -10,20 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Result of enqueueing the resume-pending wake item. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionQueueEnqueueResumePendingResult( +record QueueEnqueueResumePendingResult( /** True when a wake item was newly queued. */ @JsonProperty("queued") Boolean queued ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueFinishDeferredIdleDrainResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueFinishDeferredIdleDrainResult.java similarity index 89% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueFinishDeferredIdleDrainResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueFinishDeferredIdleDrainResult.java index 1e6cc52577..19bd1c2994 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueFinishDeferredIdleDrainResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueFinishDeferredIdleDrainResult.java @@ -16,14 +16,15 @@ /** * Action selected by the native deferred-idle drain. * - * @apiNote This method is experimental and may change in a future version. + * @apiNote This type is experimental and may change in a future version. + * * @since 1.0.0 */ @CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionQueueFinishDeferredIdleDrainResult( +record QueueFinishDeferredIdleDrainResult( /** One of none, processQueue, or emitSessionIdle. */ @JsonProperty("action") String action, /** Whether the deferred idle was caused by an aborted foreground turn. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueHasPendingResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueHasPendingResult.java similarity index 83% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueHasPendingResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueHasPendingResult.java index 5373856a45..d71abac9c2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueHasPendingResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueHasPendingResult.java @@ -10,20 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Whether the native queue has pending work. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionQueueHasPendingResult( +record QueueHasPendingResult( /** True when queued or immediate native work is pending. */ @JsonProperty("hasPending") Boolean hasPending ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueConsumeSystemNotificationsResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueRemoveMostRecentResult.java similarity index 83% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueConsumeSystemNotificationsResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueRemoveMostRecentResult.java index bbc3715880..ec0ff45f5f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueConsumeSystemNotificationsResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueRemoveMostRecentResult.java @@ -10,20 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Indicates whether a user-facing pending item was removed. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionQueueConsumeSystemNotificationsResult( +public record QueueRemoveMostRecentResult( /** True if a user-facing pending item was removed (LIFO across both queues); false when no removable items remained. */ @JsonProperty("removed") Boolean removed ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueSnapshotResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueSnapshotResult.java similarity index 89% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueSnapshotResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueSnapshotResult.java index 708d9ef1df..6712e014c5 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueSnapshotResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueueSnapshotResult.java @@ -10,21 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; /** * Internal snapshot of native queue state for local session orchestration. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionQueueSnapshotResult( +record QueueSnapshotResult( /** Queue item identifier of a model switch that has been dequeued but not yet applied. */ @JsonProperty("inFlightModelChangeId") String inFlightModelChangeId, /** User-facing pending items in FIFO order. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxHostCapability.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxHostCapability.java index 000628e137..e3efc94513 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxHostCapability.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxHostCapability.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * Whether this host can run one sandbox policy feature. A session whose effective policy uses an unsupported feature fails each sandboxed command with `reason`. + * Whether this host can run one sandbox policy feature. A session whose effective policy uses an unsupported feature fails each sandboxed command with `reason`, except `filesystem_enumeration`, whose absence degrades sandboxed PowerShell instead of failing it. * * @since 1.0.0 */ @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record SandboxHostCapability( - /** The policy feature, as an extensible string: ignore names you do not recognize. Known values: `network` (sandboxed commands can reach the network; on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns), `network_filtering` (host rules and the sandbox proxy; on Linux this needs the same tooling as `network`; on Windows it needs Process Security Environment 1.1 host-loopback support, and a policy that uses it must also set `network.allowLocalNetwork`), `denied_paths` (native enforcement of `filesystem.deniedPaths`), and `shell` (shell commands inside the sandbox; on Windows this needs Process Security Environment 1.1 filesystem enumeration support). */ + /** The policy feature, as an extensible string: ignore names you do not recognize. Known values: `network` (sandboxed commands can reach the network; on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns), `network_filtering` (host rules and the sandbox proxy; on Linux this needs the same tooling as `network`; on Windows it needs Process Security Environment 1.1 host-loopback support, and a policy that uses it must also set `network.allowLocalNetwork`), `denied_paths` (native enforcement of `filesystem.deniedPaths`), `shell` (shell commands inside the sandbox), and `filesystem_enumeration` (enumerate-only filesystem grants; on Windows this needs Process Security Environment 1.1 filesystem enumeration support, and without it sandboxed PowerShell still runs but cannot resolve its current location; other platforms always report it). */ @JsonProperty("name") String name, /** Whether this host can run the feature. */ @JsonProperty("supported") Boolean supported, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaCreateParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaCreateParams.java new file mode 100644 index 0000000000..ac0872b397 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaCreateParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaCreateParams( + /** 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. */ + @JsonProperty("sandboxConfig") SandboxConfig sandboxConfig +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaCreateResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaCreateResult.java new file mode 100644 index 0000000000..a4d2f12cc2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaCreateResult.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Result of creating the persistent certificate authority of the sandbox credential proxy. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaCreateResult( + /** Absolute path of the public certificate of the certificate authority, in PEM format. */ + @JsonProperty("certificatePath") String certificatePath +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaGetStatusParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaGetStatusParams.java new file mode 100644 index 0000000000..6f654fb053 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaGetStatusParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaGetStatusParams( + /** 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. */ + @JsonProperty("sandboxConfig") SandboxConfig sandboxConfig +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaGetStatusResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaGetStatusResult.java new file mode 100644 index 0000000000..c01ac9077d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaGetStatusResult.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Status of the persistent certificate authority of the sandbox credential proxy. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaGetStatusResult( + /** The state of the certificate authority. */ + @JsonProperty("state") SandboxProxyCaState state, + /** Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. */ + @JsonProperty("detail") String detail, + /** 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. */ + @JsonProperty("canInstall") Boolean canInstall +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRemoveResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRemoveResult.java new file mode 100644 index 0000000000..8e6dd46d30 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRemoveResult.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Status of the persistent certificate authority of the sandbox credential proxy. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaRemoveResult( + /** The state of the certificate authority. */ + @JsonProperty("state") SandboxProxyCaState state, + /** Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. */ + @JsonProperty("detail") String detail, + /** 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. */ + @JsonProperty("canInstall") Boolean canInstall +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRotateParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRotateParams.java new file mode 100644 index 0000000000..2965bb3151 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRotateParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaRotateParams( + /** 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. */ + @JsonProperty("sandboxConfig") SandboxConfig sandboxConfig +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRotateResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRotateResult.java new file mode 100644 index 0000000000..b53cc5aa68 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaRotateResult.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Status of the persistent certificate authority of the sandbox credential proxy. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaRotateResult( + /** The state of the certificate authority. */ + @JsonProperty("state") SandboxProxyCaState state, + /** Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. */ + @JsonProperty("detail") String detail, + /** 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. */ + @JsonProperty("canInstall") Boolean canInstall +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaState.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaState.java new file mode 100644 index 0000000000..4a4dc15662 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaState.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * State of the persistent certificate authority of the sandbox credential proxy. + * + * @apiNote This type is experimental and may change in a future version. + * + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SandboxProxyCaState { + /** The {@code unsupported} variant. */ + UNSUPPORTED("unsupported"), + /** The {@code notInstalled} variant. */ + NOTINSTALLED("notInstalled"), + /** The {@code installed} variant. */ + INSTALLED("installed"), + /** The {@code error} variant. */ + ERROR("error"); + + private final String value; + SandboxProxyCaState(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SandboxProxyCaState fromValue(String value) { + for (SandboxProxyCaState v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SandboxProxyCaState value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaTrustParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaTrustParams.java new file mode 100644 index 0000000000..b54e2ce93e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaTrustParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaTrustParams( + /** 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. */ + @JsonProperty("sandboxConfig") SandboxConfig sandboxConfig +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaTrustResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaTrustResult.java new file mode 100644 index 0000000000..24e462e16b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxProxyCaTrustResult.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Status of the persistent certificate authority of the sandbox credential proxy. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SandboxProxyCaTrustResult( + /** The state of the certificate authority. */ + @JsonProperty("state") SandboxProxyCaState state, + /** Human-readable reason for the state. On `installed` or `notInstalled`, present only when the certificate authority must be rotated, and then says why. */ + @JsonProperty("detail") String detail, + /** 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. */ + @JsonProperty("canInstall") Boolean canInstall +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ScheduleAddResult.java similarity index 84% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/ScheduleAddResult.java index 021c784c37..9df295e3d8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ScheduleAddResult.java @@ -10,20 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Result of registering or re-arming a scheduled prompt. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionScheduleAddResult( +record ScheduleAddResult( /** The registered or updated schedule entry. */ @JsonProperty("entry") ScheduleEntry entry, /** User-facing validation error, when registration failed. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleHasSelfPacedResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ScheduleHasSelfPacedResult.java similarity index 83% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleHasSelfPacedResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/ScheduleHasSelfPacedResult.java index 84c8e7a501..daf5830d1e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleHasSelfPacedResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ScheduleHasSelfPacedResult.java @@ -10,20 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Whether the session currently has an active self-paced schedule. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionScheduleHasSelfPacedResult( +record ScheduleHasSelfPacedResult( /** True when at least one active schedule is self-paced. */ @JsonProperty("hasSelfPaced") Boolean hasSelfPaced ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountApi.java index 0adbf18092..714ef4ebb6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountApi.java @@ -28,7 +28,7 @@ public final class ServerAccountApi { } /** - * Optional opaque account selection or compatibility GitHub token used to look up quota. + * Gets Copilot quota usage for the current or opaquely selected authenticated user. *

* Invokes the method with no params, applying the runtime defaults. * @@ -41,7 +41,7 @@ public CompletableFuture getQuota() { } /** - * Optional opaque account selection or compatibility GitHub token used to look up quota. + * Gets Copilot quota usage for the current or opaquely selected authenticated user. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -52,7 +52,7 @@ public CompletableFuture getQuota(AccountGetQuotaParams p } /** - * Current authentication state + * Gets the currently active authentication credentials from the global auth manager. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -63,7 +63,7 @@ public CompletableFuture getCurrentAuth() { } /** - * List of all authenticated users + * Gets all authenticated users available for account switching. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -74,7 +74,7 @@ public CompletableFuture> getAllUsers() { } /** - * Credentials to validate and store. Omit login to resolve the authenticated user from the token. + * Validates and stores authentication credentials. When login is omitted, resolves the authenticated user from the token before persistence. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -85,7 +85,7 @@ public CompletableFuture login(AccountLoginParams params) { } /** - * User to log out + * Removes user authentication from keychain and persisted state. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountsApi.java deleted file mode 100644 index 20473515d2..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAccountsApi.java +++ /dev/null @@ -1,40 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.github.copilot.CopilotExperimental; -import java.util.concurrent.CompletableFuture; -import javax.annotation.processing.Generated; - -/** - * API methods for the {@code accounts} namespace. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class ServerAccountsApi { - - private final RpcCaller caller; - - /** @param caller the RPC transport function */ - ServerAccountsApi(RpcCaller caller) { - this.caller = caller; - } - - /** - * OneAuth token request supplied by a trusted host application. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ - @CopilotExperimental - public CompletableFuture acquireEntraToken(AccountsAcquireEntraTokenParams params) { - return caller.invoke("accounts.acquireEntraToken", params, EntraTokenAcquireResult.class); - } - -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentRegistryApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentRegistryApi.java index f398fb2dcb..a96ce680d7 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentRegistryApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentRegistryApi.java @@ -27,7 +27,7 @@ public final class ServerAgentRegistryApi { } /** - * Inputs to spawn a managed-server child via the controller's spawn delegate. + * Spawns a managed-server child with the supplied configuration and returns a discriminated-union result. The caller (typically the CLI controller) is responsible for attaching to the spawned child and sending any follow-up prompt. When the controller-local spawn gate is closed the server returns JSON-RPC MethodNotFound. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentsApi.java index 0c621a3624..0fd067f72f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerAgentsApi.java @@ -27,7 +27,7 @@ public final class ServerAgentsApi { } /** - * Optional project paths to include in agent discovery. + * Discovers custom agents across user, project, plugin, and remote sources. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture discover(AgentsDiscoverParams par } /** - * Optional project paths to include when enumerating agent discovery directories. + * Returns the canonical directories where a client may create custom agents that the runtime will recognize, including ones that do not exist yet. Project directories become active once created. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCatalogApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCatalogApi.java index 32e7963e35..f7d7c2fe1b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCatalogApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCatalogApi.java @@ -28,7 +28,7 @@ public final class ServerCatalogApi { } /** - * A bounded catalog search. Both the query length and the result count are capped by the schema so a caller cannot request an unbounded scan. + * Requests a bounded catalog search. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with search available returns inert candidate summaries, each with an opaque single-use handle scoped to this runtime instance; a runtime without it returns the typed search-unavailable result. Public authorities may be searched anonymously, while an authority that requires credentials yields the typed authentication-required result. All returned text, URLs, and package metadata are untrusted external data and can never trigger instructions, tools, or installation. Read-only: nothing is installed, configured, or persisted. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -39,7 +39,7 @@ public CompletableFuture search(CatalogSearchParams params) } /** - * A bounded catalog search. Both the query length and the result count are capped by the schema so a caller cannot request an unbounded scan. + * Requests a bounded catalog search. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with search available returns inert candidate summaries, each with an opaque single-use handle scoped to this runtime instance; a runtime without it returns the typed search-unavailable result. Public authorities may be searched anonymously, while an authority that requires credentials yields the typed authentication-required result. All returned text, URLs, and package metadata are untrusted external data and can never trigger instructions, tools, or installation. Read-only: nothing is installed, configured, or persisted. *

* Accepts the extensible request, including inputs added after the params record. * @@ -52,7 +52,7 @@ public CompletableFuture search(CatalogSearchRequest reques } /** - * Terminates one retained catalog selection group through an opaque reference previously returned by the model-safe search projection. + * Terminates one retained catalog selection group. A selected outcome returns the native host a fresh single-use candidate handle plus the original searchId for a later explicit mcp.planInstall call; non-selected outcomes release the group without producing a planning input. Candidate state, cards, URLs, credentials and private identifiers remain inside the runtime. The model-facing catalog_select tool projects the result separately and never exposes the candidate handle or searchId. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCommandsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCommandsApi.java index efa5317ea4..82c5fad8c4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCommandsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerCommandsApi.java @@ -27,7 +27,7 @@ public final class ServerCommandsApi { } /** - * Slash commands available in the session, after applying any include/exclude filters. + * Lists the well-known built-in slash commands that work as the first message in a new session (e.g. /plan, /env), without requiring an active session. Commands that depend on session state, authentication, or a synced session are omitted. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerEnvironmentsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerEnvironmentsApi.java new file mode 100644 index 0000000000..a8220ef6fc --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerEnvironmentsApi.java @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code environments} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerEnvironmentsApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerEnvironmentsApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Lists GitHub Mission Control environments visible to the authenticated identity. Does not require a running host and excludes host relay credentials. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture list(EnvironmentsListParams params) { + return caller.invoke("environments.list", params, EnvironmentsListResult.class); + } + + /** + * Gets safe discovery information for a GitHub Mission Control environment without requiring a running host. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture get(EnvironmentsGetParams params) { + return caller.invoke("environments.get", params, EnvironmentsGetResult.class); + } + + /** + * Deletes a user-managed GitHub Mission Control environment. GitHub-managed environments cannot be deleted. Does not stop a running host, which may register again. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture delete(EnvironmentsDeleteParams params) { + return caller.invoke("environments.delete", params, Void.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerExtensionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerExtensionsApi.java index 7bc74b441f..385fc3bbc2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerExtensionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerExtensionsApi.java @@ -27,7 +27,7 @@ public final class ServerExtensionsApi { } /** - * Extensions discovered from persisted Copilot home state and their effective loading mode. Launch-scoped additional plugins are not included. + * Discovers user and enabled installed-plugin extensions from persisted Copilot home state, including enablement preferences. Launch-scoped additional plugins are not included. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture discover() { } /** - * Source-qualified extension identifiers to persistently enable for future sessions. + * Persistently enables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.enable to update them. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture enable(ExtensionsEnableParams params) { } /** - * Source-qualified extension identifiers to persistently disable for future sessions. + * Persistently disables extension IDs for future sessions. Active sessions are unchanged; use session.extensions.disable to update them. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java index b6f69bf7dc..f41fbd63ac 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java @@ -27,7 +27,7 @@ public final class ServerHooksApi { } /** - * Optional project paths and host-exclusion behavior for server-scoped hook discovery. + * Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHostApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHostApi.java index 328f88d5f2..a921cdf945 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHostApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHostApi.java @@ -27,7 +27,7 @@ public final class ServerHostApi { } /** - * Publishes a resident session attached to the listener's owning connection. + * Publishes an attached resident session for this listener's lifetime without copying it. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,18 +38,18 @@ public CompletableFuture publishSession(HostPublishSes } /** - * One application-owned session handoff, requested by the supervised hosting participant. + * Requests app-owned materialization over the owning SDK participant. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - CompletableFuture createSession(HostCreateSessionParams params) { - return caller.invoke("host.createSession", params, HostCreateSessionResult.class); + CompletableFuture createSession(HostCreateSessionParams params) { + return caller.invoke("host.createSession", params, HostSessionCreateResult.class); } /** - * Ends one participation, not the application's session lifetime. + * Releases app ownership retention after AHP detaches. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ CompletableFuture releaseSession(HostReleaseSessionParams params) { } /** - * Starts a supervised AHP listener in the runtime's configured working directory. + * Starts a connection-owned AHP host with explicit localServer and/or githubEnvironment transports as a supervised SDK participant. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -71,7 +71,7 @@ public CompletableFuture start(HostStartParams params) { } /** - * Stops a connection-owned listener and joins its teardown. + * Stops a listener owned by this SDK connection and joins its cleanup without deleting sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -82,18 +82,29 @@ public CompletableFuture dispose(HostDisposeParams params) { } /** - * Empty acknowledgement for a completed host lifecycle operation. + * Resolves current authenticated credentials and remote-control policy only for the runtime-owned Mission Control hosting participant. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - CompletableFuture getConfiguration() { - return caller.invoke("host.getConfiguration", java.util.Map.of(), HostGetConfigurationResult.class); + CompletableFuture getEnvironmentCredentials() { + return caller.invoke("host.getEnvironmentCredentials", java.util.Map.of(), HostEnvironmentCredentials.class); } /** - * Readiness reported by the supervised hosting participant on its own SDK connection. + * Returns listener settings only to the supervised hosting participant over its SDK connection. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture getConfiguration() { + return caller.invoke("host.getConfiguration", java.util.Map.of(), HostConfiguration.class); + } + + /** + * Reports a supervised hosting participant's bound AHP endpoint after its SDK handshake. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerInstructionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerInstructionsApi.java index 70eb5b0211..23d8c7e2de 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerInstructionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerInstructionsApi.java @@ -27,7 +27,7 @@ public final class ServerInstructionsApi { } /** - * Optional project paths to include in instruction discovery. + * Discovers instruction sources across user, repository, and plugin sources. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture discover(InstructionsDiscov } /** - * Optional project paths to include when enumerating instruction discovery targets. + * Returns the canonical files and directories where a client may create custom instructions that the runtime will recognize, including ones that do not exist yet. Repository targets become active once created. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerLlmInferenceApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerLlmInferenceApi.java index 4b7663f1f3..753eccb9f3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerLlmInferenceApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerLlmInferenceApi.java @@ -27,7 +27,7 @@ public final class ServerLlmInferenceApi { } /** - * Indicates whether the calling client was registered as the LLM inference provider. + * Registers an SDK client as the LLM inference callback provider. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture setProvider() { } /** - * Response head. + * Delivers the response head (status + headers) for an in-flight request, correlated by the requestId the runtime supplied in httpRequestStart. Must be called exactly once per request before any httpResponseChunk frames. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture httpResponseStart( } /** - * A response body chunk or terminal error. + * Delivers a body byte range (or a terminal transport error) for an in-flight response, correlated by requestId. Set `end` true on the last chunk. When `error` is set the response terminates with a transport-level failure and the runtime raises an APIConnectionError. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java index e6013c870d..a686df708c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java @@ -27,7 +27,7 @@ public final class ServerManagedSettingsApi { } /** - * Validated device-managed settings discovered before a session exists. + * Discovers device-managed settings from production MDM and managed-file sources, validates them against the runtime-owned managed-settings schema, and returns the canonical JSON without requiring a session. `managedSettings.resolve` returns the same device settings together with the account's server policy. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture read() { } /** - * Invokes {@code managedSettings.clearCache}. + * Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing "sync account policy" should call `managedSettings.resolve` or start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -48,4 +48,61 @@ public CompletableFuture clearCache() { return caller.invoke("managedSettings.clearCache", java.util.Map.of(), Void.class); } + /** + * Resolves the effective enterprise managed settings without a session, from the device channel and, when an account is available, the account's server policy through the same per-account cache sessions use. A cached server policy less than an hour old is used without a fetch; otherwise the policy is fetched, and when the fetch fails a cached policy up to 24 hours old is used instead, unless `forceRemoteSettingsRefresh` requires a live fetch. With no account requested or signed in, it reports device policy only; signing out removes the account's cached policy. It can fetch server policy over the network when the cache is stale, so call it off latency-critical paths such as startup rather than before listing models. The policy helper is not run. `layers` lists each channel's document before merging, and `values` and `meta` carry typed effective values and their lock state for the keys typed so far. + *

+ * Invokes the method with no params, applying the runtime defaults. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture resolve() { + return resolve(null); + } + + /** + * Resolves the effective enterprise managed settings without a session, from the device channel and, when an account is available, the account's server policy through the same per-account cache sessions use. A cached server policy less than an hour old is used without a fetch; otherwise the policy is fetched, and when the fetch fails a cached policy up to 24 hours old is used instead, unless `forceRemoteSettingsRefresh` requires a live fetch. With no account requested or signed in, it reports device policy only; signing out removes the account's cached policy. It can fetch server policy over the network when the cache is stale, so call it off latency-critical paths such as startup rather than before listing models. The policy helper is not run. `layers` lists each channel's document before merging, and `values` and `meta` carry typed effective values and their lock state for the keys typed so far. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture resolve(ManagedSettingsResolveParams params) { + return caller.invoke("managedSettings.resolve", params == null ? java.util.Map.of() : params, ManagedSettingsResolveResult.class); + } + + /** + * Returns the managed-settings authoring JSON schema with descriptive `x-composition` annotations aligned with the shared settings-engine vocabulary. These annotations are not a complete runtime composition contract: model, effortLevel, and contextTier remain coupled. Use `managedSettings.compose` for the runtime's effective result. Performs no I/O. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture schema() { + return caller.invoke("managedSettings.schema", java.util.Map.of(), ManagedSettingsSchemaResult.class); + } + + /** + * Validates a candidate managed-settings document the way the runtime validates delivered policy, without applying it. Reports errors that would reject the document, warnings for content the runtime ignores, and the canonical document it would apply. Document text nested more than 64 levels deep is rejected. Performs no I/O. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture validate(ManagedSettingsValidateParams params) { + return caller.invoke("managedSettings.validate", params, ManagedSettingsValidateResult.class); + } + + /** + * Merges candidate managed-settings documents for the device, server, and policy-helper channels into the effective settings the runtime would enforce on this host, using the same precedence and composition rules as live resolution, without applying them. Like live resolution, a server's advisory sandbox force-enable is declined on a host that cannot run the sandbox. Does not fetch policy or read policy files, but may perform blocking OS or subprocess probes for sandbox support. Preview documents are limited to 1 MiB and 64 levels of nesting. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture compose(ManagedSettingsComposeParams params) { + return caller.invoke("managedSettings.compose", params, ManagedSettingsComposeResult.class); + } + } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpApi.java index 9557a2f4ac..553a644214 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpApi.java @@ -35,7 +35,7 @@ public final class ServerMcpApi { } /** - * Optional working directory used as context for MCP server discovery. + * Discovers MCP servers from user, workspace, plugin, and builtin sources. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -46,7 +46,7 @@ public CompletableFuture discover(McpDiscoverParams params) { } /** - * A side-effect-free request for an MCP install plan. Computing a plan never writes configuration, stores a secret, or reloads MCP servers. + * Requests a side-effect-free MCP install plan from a catalog candidate handle or a caller-supplied card. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with planning available returns a normalised plan and opaque single-use plan handle; a runtime without it returns the typed planning-unavailable result. A completed plan reports resource identity, provenance, eligible transport choices, the user-scope target, required typed values and secret placeholders, the policy result, the configuration changes installing would make, and whether a reload would be needed. Planning never writes configuration, stores a secret, or reloads MCP servers, so abandoning a plan needs no call and leaves nothing behind. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -57,7 +57,7 @@ public CompletableFuture planInstall(McpPlanInstallParams } /** - * A side-effect-free request for an MCP install plan. Computing a plan never writes configuration, stores a secret, or reloads MCP servers. + * Requests a side-effect-free MCP install plan from a catalog candidate handle or a caller-supplied card. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with planning available returns a normalised plan and opaque single-use plan handle; a runtime without it returns the typed planning-unavailable result. A completed plan reports resource identity, provenance, eligible transport choices, the user-scope target, required typed values and secret placeholders, the policy result, the configuration changes installing would make, and whether a reload would be needed. Planning never writes configuration, stores a secret, or reloads MCP servers, so abandoning a plan needs no call and leaves nothing behind. *

* Accepts the extensible request, including inputs added after the params record. * @@ -70,7 +70,7 @@ public CompletableFuture planInstall(McpPlanInstallRequest } /** - * Side-effect-free preparation of one original bound remote MCP choice. + * Consumes a bound catalogue plan and retains one exact fully resolved personal remote MCP operation requiring no supplied values or configured secrets. Returns its runtime operation ID and original expiry before any confirmation, activation, writer initialisation or installation effect. Register the original connection, operation and selected-session binding before calling applyInstall. Missing lower owned admission is unavailable, never a raw-config fallback. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -81,7 +81,7 @@ public CompletableFuture prepareInstall(McpPrep } /** - * Applies exactly one previously prepared operation on its original connection. + * Consumes a retained prepared MCP operation once, revalidates its original authority, requests explicit human consent through installations.confirm on the original connection, then revalidates source and applies the sealed transaction. An uncertain result requires original-operation inspection or recovery, never replay. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -92,7 +92,7 @@ public CompletableFuture applyInstall(McpApplyInstallPara } /** - * Read-only preparation of one owned removal under fresh selected-session authority. + * Prepares a read-only removal plan for an exact owned receipt under the selected existing session. Returns the original operation ID before confirmation; neither planning nor abandonment changes configuration or shared OAuth credentials. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -103,7 +103,7 @@ public CompletableFuture planUninstall(McpPlanU } /** - * One-use application of the exact retained removal plan. + * Consumes the original owned-removal plan once and requests fresh exact human confirmation on its original connection. Drift is refused; unrelated manual configuration and shared OAuth credentials are preserved. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpConfigApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpConfigApi.java index 6d3510f515..7271e94e29 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpConfigApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpConfigApi.java @@ -27,7 +27,7 @@ public final class ServerMcpConfigApi { } /** - * User-configured MCP servers, keyed by server name. + * Lists MCP servers from user configuration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture list() { } /** - * MCP server name and configuration to add to user configuration. + * Adds an MCP server to user configuration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture add(McpConfigAddParams params) { } /** - * MCP server name and replacement configuration to write to user configuration. + * Updates an MCP server in user configuration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ public CompletableFuture update(McpConfigUpdateParams params) { } /** - * MCP server name to remove from user configuration. + * Removes an MCP server from user configuration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -71,7 +71,7 @@ public CompletableFuture remove(McpConfigRemoveParams params) { } /** - * MCP server names to enable for new sessions. + * Enables MCP servers in user configuration for new sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -82,7 +82,7 @@ public CompletableFuture enable(McpConfigEnableParams params) { } /** - * MCP server names to disable for new sessions. + * Disables MCP servers in user configuration for new sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -93,7 +93,7 @@ public CompletableFuture disable(McpConfigDisableParams params) { } /** - * Invokes {@code mcp.config.reload}. + * Drops this runtime process's in-memory MCP server-definition cache so the next MCP config read observes disk. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpInstallationsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpInstallationsApi.java index ec99b766e9..6055d0fd3f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpInstallationsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerMcpInstallationsApi.java @@ -27,7 +27,7 @@ public final class ServerMcpInstallationsApi { } /** - * New-work inventory or recovery request under an explicitly selected existing session. + * Reads receipt-owned MCP inventory for the selected account and host without activating servers or reconstructing missing ownership. Configuration ownership does not prove session-specific usability. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture list(McpInstallationsL } /** - * New-work inventory or recovery request under an explicitly selected existing session. + * Reconciles already-confirmed durable MCP transactions, then inspects owned inventory. Does not replay apply or reconstruct deleted ownership metadata; unresolved or unsafe evidence remains an explicit refusal. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture recover(McpInstallatio } /** - * Existing-operation control. A new session selector is deliberately not accepted. + * Inspects a known operation only on its original connection. Remains available after account or selected-session loss; does not acquire new authority or rebind an operation. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ public CompletableFuture status(McpInstallation } /** - * Existing-operation control. A new session selector is deliberately not accepted. + * Requests cancellation of a known operation on its original connection, including before apply or confirmation. Already-started effects retain their transaction lease and report an honest terminal or recovery outcome. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerModelsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerModelsApi.java index e4fd3cbc53..491340551d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerModelsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerModelsApi.java @@ -27,7 +27,7 @@ public final class ServerModelsApi { } /** - * Optional opaque account selection or compatibility GitHub token used to list models. + * Lists Copilot models available to the authenticated user. *

* Invokes the method with no params, applying the runtime defaults. * @@ -40,7 +40,7 @@ public CompletableFuture list() { } /** - * Optional opaque account selection or compatibility GitHub token used to list models. + * Lists Copilot models available to the authenticated user. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -51,7 +51,7 @@ public CompletableFuture list(ModelsListParams params) { } /** - * The running runtime's complete catalog of well-known built-in model IDs, including supported models and additional IDs with built-in metadata. + * Returns the running runtime's complete catalog of well-known built-in model IDs without authentication or network access. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsApi.java index da5ac6c0e8..4fd1a839b0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsApi.java @@ -34,7 +34,7 @@ public final class ServerPluginsApi { } /** - * Plugins installed in user/global state. + * Lists plugins installed in user/global state. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -45,7 +45,7 @@ public CompletableFuture list() { } /** - * Plugin source and optional working directory for relative-path resolution. + * Installs a plugin from a marketplace, GitHub repo, URL, or local path. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -56,7 +56,7 @@ public CompletableFuture install(PluginsInstallParams para } /** - * Name (or spec) of the plugin to uninstall. + * Uninstalls an installed plugin. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -67,7 +67,7 @@ public CompletableFuture uninstall(PluginsUninstallParams params) { } /** - * Name (or spec) of the plugin to update. + * Updates an installed plugin to its latest published version. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -78,7 +78,7 @@ public CompletableFuture update(PluginsUpdateParams params) } /** - * Result of updating all installed plugins. + * Updates every installed plugin to its latest published version. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -89,7 +89,7 @@ public CompletableFuture updateAll() { } /** - * Plugin names (or specs) to enable, plus the optional working directory the repository-controlled guard is evaluated against. + * Enables installed plugins for new sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -100,7 +100,7 @@ public CompletableFuture enable(PluginsEnableParams params) { } /** - * Plugin names (or specs) to disable, plus the optional working directory the repository-controlled guard is evaluated against. + * Disables installed plugins for new sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsBuiltinApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsBuiltinApi.java index 0610fcb28e..b35a77a817 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsBuiltinApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsBuiltinApi.java @@ -27,7 +27,7 @@ public final class ServerPluginsBuiltinApi { } /** - * Trusted built-in plugin directories to use for this runtime process. + * Replaces this server's trusted built-in plugin directories while no sessions are active. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsMarketplacesApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsMarketplacesApi.java index e01bbeeec7..93adce4997 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsMarketplacesApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerPluginsMarketplacesApi.java @@ -27,7 +27,7 @@ public final class ServerPluginsMarketplacesApi { } /** - * All registered marketplaces, including built-in defaults. + * Lists all registered marketplaces (defaults + user-added). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture list() { } /** - * Marketplace source and optional working directory for relative-path resolution. + * Registers a new marketplace from a source (owner/repo, URL, or local path). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture add(PluginsMarketplacesAd } /** - * Name of the marketplace to remove and an optional force flag. + * Removes a previously-registered marketplace. When the marketplace has dependent plugins and `force` is not set, the marketplace is left intact and the result lists the dependents so the caller can decide whether to retry with `force=true`. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ public CompletableFuture remove(PluginsMarketpl } /** - * Name of the marketplace whose plugin catalog to fetch. + * Lists plugins advertised by a registered marketplace. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -71,7 +71,7 @@ public CompletableFuture browse(PluginsMarketpl } /** - * Optional marketplace name; omit to refresh all. + * Re-fetches one or all registered marketplace catalogs. *

* Invokes the method with no params, applying the runtime defaults. * @@ -84,7 +84,7 @@ public CompletableFuture refresh() { } /** - * Optional marketplace name; omit to refresh all. + * Re-fetches one or all registered marketplace catalogs. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java index f04da408c1..809b5f2ec3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java @@ -25,6 +25,8 @@ public final class ServerRpc { private final RpcCaller caller; + /** API methods for the {@code environments} namespace. */ + public final ServerEnvironmentsApi environments; /** API methods for the {@code host} namespace. */ public final ServerHostApi host; /** API methods for the {@code hooks} namespace. */ @@ -69,8 +71,6 @@ public final class ServerRpc { public final ServerSessionsApi sessions; /** API methods for the {@code agentRegistry} namespace. */ public final ServerAgentRegistryApi agentRegistry; - /** API methods for the {@code accounts} namespace. */ - public final ServerAccountsApi accounts; /** * Creates a new server RPC client. @@ -79,6 +79,7 @@ public final class ServerRpc { */ public ServerRpc(RpcCaller caller) { this.caller = caller; + this.environments = new ServerEnvironmentsApi(caller); this.host = new ServerHostApi(caller); this.hooks = new ServerHooksApi(caller); this.models = new ServerModelsApi(caller); @@ -101,11 +102,10 @@ public ServerRpc(RpcCaller caller) { this.llmInference = new ServerLlmInferenceApi(caller); this.sessions = new ServerSessionsApi(caller); this.agentRegistry = new ServerAgentRegistryApi(caller); - this.accounts = new ServerAccountsApi(caller); } /** - * Optional message to echo back to the caller. + * Checks server responsiveness and returns protocol information. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -116,18 +116,18 @@ public CompletableFuture ping(PingParams params) { } /** - * Connection-level opt-ins for the `server.connect` handshake. Transport authentication is consumed by the native protocol boundary before dispatch. + * Performs the SDK server connection handshake and validates the optional connection token. Marked internal because this is JSON-RPC transport plumbing invoked automatically by an SDK client's own `connect()` wrapper, not a user-facing method. Stays internal as long as the SDK client owns the handshake; would only become public if the SDK ever exposed the raw schema surface to consumers without a connection wrapper. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture connect(ConnectParams params) { + CompletableFuture connect(ConnectParams params) { return caller.invoke("connect", params, ConnectResult.class); } /** - * Invokes {@code registerExtensionLaunchProvider}. + * Registers the calling SDK client as the per-entrypoint extension launch provider. Call before creating any sessions. When omitted, the runtime uses its built-in extension launcher. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRuntimeApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRuntimeApi.java index e57db70946..d09f784d8d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRuntimeApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRuntimeApi.java @@ -27,7 +27,7 @@ public final class ServerRuntimeApi { } /** - * Invokes {@code runtime.shutdown}. + * Gracefully shuts down an SDK-owned runtime. The response is sent only after cleanup completes; callers may then terminate the owned runtime process. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxApi.java index 9c33cda35c..1484140f6a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxApi.java @@ -21,13 +21,17 @@ public final class ServerSandboxApi { private final RpcCaller caller; + /** API methods for the {@code sandbox.proxyCa} sub-namespace. */ + public final ServerSandboxProxyCaApi proxyCa; + /** @param caller the RPC transport function */ ServerSandboxApi(RpcCaller caller) { this.caller = caller; + this.proxyCa = new ServerSandboxProxyCaApi(caller); } /** - * Whether the host running this runtime can run the command sandbox. The runtime checks `supported` once per process. A capability answer can change while the process runs, for example after the user installs a missing package. + * Reports whether the host running this runtime can run the command sandbox, without starting a session or spawning a sandboxed command. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxProxyCaApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxProxyCaApi.java new file mode 100644 index 0000000000..0cc59738e9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSandboxProxyCaApi.java @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code sandbox.proxyCa} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerSandboxProxyCaApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerSandboxProxyCaApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture getStatus(SandboxProxyCaGetStatusParams params) { + return caller.invoke("sandbox.proxyCa.getStatus", params, SandboxProxyCaGetStatusResult.class); + } + + /** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture create(SandboxProxyCaCreateParams params) { + return caller.invoke("sandbox.proxyCa.create", params, SandboxProxyCaCreateResult.class); + } + + /** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture rotate(SandboxProxyCaRotateParams params) { + return caller.invoke("sandbox.proxyCa.rotate", params, SandboxProxyCaRotateResult.class); + } + + /** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture trust(SandboxProxyCaTrustParams params) { + return caller.invoke("sandbox.proxyCa.trust", params, SandboxProxyCaTrustResult.class); + } + + /** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture remove() { + return caller.invoke("sandbox.proxyCa.remove", java.util.Map.of(), SandboxProxyCaRemoveResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSecretsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSecretsApi.java index 7f17687818..ebdcac5e9e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSecretsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSecretsApi.java @@ -27,7 +27,7 @@ public final class ServerSecretsApi { } /** - * Secret values to add to the redaction filter. + * Registers secret values for redaction in session logs and exports. The SDK calls this to inject dynamically generated secret values (e.g., OIDC tokens). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionFsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionFsApi.java index 872702997a..ff48cdfb76 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionFsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionFsApi.java @@ -27,7 +27,7 @@ public final class ServerSessionFsApi { } /** - * Initial working directory, session-state path layout, and path conventions used to register the calling SDK client as the session filesystem provider. A registered provider is authoritative for path interpretation and filesystem facts used by workspace permission validation. Paths are interpreted lexically; home-relative paths (`~` and `~/...`) and Windows drive-relative paths such as `C:foo` are unsupported. Until provider-side canonicalization is supported, providers must not expose symlinks inside allowed roots that escape those roots. + * Registers an SDK client as the session filesystem provider. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java index bc65f86fcb..eb3546a191 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java @@ -28,7 +28,7 @@ public final class ServerSessionsApi { } /** - * Open a session by creating, resuming, attaching, connecting to a remote, or handing off. + * Creates or resumes a local session and returns the opened session ID. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -39,7 +39,7 @@ public CompletableFuture open(SessionsOpenParams params) { } /** - * Source session identifier to fork from, optional event-ID boundary, and optional friendly name for the new session. + * Creates a new session by forking persisted history from an existing session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -50,7 +50,7 @@ public CompletableFuture fork(SessionsForkParams params) { } /** - * Remote session connection parameters. + * Connects to an existing remote session and exposes it as an SDK session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -61,7 +61,7 @@ public CompletableFuture connect(SessionsConnectParams pa } /** - * Optional source filter, metadata-load limit, and context filter applied to the returned sessions. + * Lists sessions, optionally filtered by source and working-directory context. Returned entries are discriminated by `isRemote`: local entries carry only the lightweight `LocalSessionMetadataValue` shape; remote entries carry the full `RemoteSessionMetadataValue` shape (repository, PR number, taskType, etc.). *

* Invokes the method with no params, applying the runtime defaults. * @@ -74,7 +74,7 @@ public CompletableFuture list() { } /** - * Optional source filter, metadata-load limit, and context filter applied to the returned sessions. + * Lists sessions, optionally filtered by source and working-directory context. Returned entries are discriminated by `isRemote`: local entries carry only the lightweight `LocalSessionMetadataValue` shape; remote entries carry the full `RemoteSessionMetadataValue` shape (repository, PR number, taskType, etc.). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -85,18 +85,18 @@ public CompletableFuture list(SessionsListParams params) { } /** - * Session ID whose persisted metadata should be read. + * Reads lightweight persisted metadata for one local session without opening it. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture getMetadata(SessionsGetMetadataParams params) { + CompletableFuture getMetadata(SessionsGetMetadataParams params) { return caller.invoke("sessions.getMetadata", params, SessionsGetMetadataResult.class); } /** - * Bounded batch request for client-owned metadata from persisted local sessions. + * Reads client-owned metadata for multiple persisted local sessions without opening them. Results preserve request order and report missing, corrupt, unsupported, or temporarily unavailable sessions independently. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -107,7 +107,7 @@ public CompletableFuture> getClientMetadata(SessionsGetClientMetada } /** - * Pagination options for reading an inactive or active local session's persisted event journal. + * Reads a page of durable events directly from a local session's persisted journal without creating, resuming, or activating the session. The first read pins the currently opened journal generation and its byte-length boundary; opaque cursor continuations remain on that generation across runtime-owned compaction, truncation, and rewrite operations, which replace the live path atomically, and events appended after the boundary are excluded. For cold hydration, await the first successful page before activation and establish lossless live-event buffering before resume; merge subsequent live events by ID, preserving persisted order and letting live payloads win. Continuations are process-local, single-use capabilities bound to the originating session and storage context and must be paged sequentially; concurrent or repeated use of the same cursor expires that duplicate read rather than reading the generation twice. A complete snapshot has cursorStatus 'ok' and hasMore false. Snapshots expire after five idle minutes, with at most eight retained per process and idle-only eviction under pressure; completion and cancelled-worker exit release their handles. No transcript copy is created, but retained handles may keep replaced files' disk blocks alive until release. Pages have a soft 1 MiB serialized event-array budget including resolved binary assets; one oversized event is returned alone to guarantee progress. Working memory also includes a record/lookahead and asset resolution; resolving the first binary reference may scan the full pinned generation to build a bounded offset index. If the snapshot expires, is evicted, is cancelled before a continuation is established, or becomes unreadable after an observable unsupported in-place shortening, the continuation returns cursorStatus 'expired' with an empty terminal page and never falls back to a different generation. A missing or initially unreadable journal is an RPC error. Persisted history excludes ephemeral events and may omit payloads that are reconstructed only for an active session; use the active session event stream for post-resume live events. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -118,18 +118,18 @@ public CompletableFuture readPersistedEvents( } /** - * Limit for non-empty local session IDs. + * Lists recent local session IDs that contain user-visible history, omitting housekeeping-only sessions. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture listNonEmptySessionIds(SessionsListNonEmptySessionIdsParams params) { + CompletableFuture listNonEmptySessionIds(SessionsListNonEmptySessionIdsParams params) { return caller.invoke("sessions.listNonEmptySessionIds", params, SessionsListNonEmptySessionIdsResult.class); } /** - * GitHub task ID to look up. + * Finds the local session bound to a GitHub task ID, if any. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -140,7 +140,7 @@ public CompletableFuture findByTaskId(SessionsFindBy } /** - * UUID prefix to resolve to a unique session ID. + * Resolves a UUID prefix to a unique session ID, if exactly one session matches. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -151,7 +151,7 @@ public CompletableFuture findByPrefix(SessionsFindBy } /** - * Optional working-directory context used to score session relevance. + * Returns the most-relevant prior session for a given working-directory context. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -162,18 +162,18 @@ public CompletableFuture getLastForContext(Sess } /** - * Session ID whose event-log file path to compute. + * Computes the absolute path to a session's persisted events.jsonl file. Internal: filesystem paths are only meaningful in-process (CLI and runtime share a filesystem). Currently used by the CLI's contribution-graph feature to read historical events directly. Remote SDK consumers must not depend on this; a proper event-query API would replace it if the contribution graph ever needed to work over the wire. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture getEventFilePath(SessionsGetEventFilePathParams params) { + CompletableFuture getEventFilePath(SessionsGetEventFilePathParams params) { return caller.invoke("sessions.getEventFilePath", params, SessionsGetEventFilePathResult.class); } /** - * Map of sessionId -> on-disk size in bytes for each session's workspace directory. + * Returns the on-disk byte size of each session's workspace directory. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -184,7 +184,7 @@ public CompletableFuture getSizes() { } /** - * Session IDs to test for live in-use locks. + * Returns the subset of the supplied session IDs that are currently held by another running process. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -195,18 +195,18 @@ public CompletableFuture checkInUse(SessionsCheckInUse } /** - * Session ID to look up the persisted remote-steerable flag for. + * Returns a session's persisted remote-steerable flag, if any has been recorded. Internal: this is CLI-specific book-keeping used by `--continue` / `--resume` to inherit the prior session's remote-steerable preference. SDK consumers that want similar behavior should manage their own persistence around start/stop calls rather than relying on this runtime-side flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture getPersistedRemoteSteerable(SessionsGetPersistedRemoteSteerableParams params) { + CompletableFuture getPersistedRemoteSteerable(SessionsGetPersistedRemoteSteerableParams params) { return caller.invoke("sessions.getPersistedRemoteSteerable", params, SessionsGetPersistedRemoteSteerableResult.class); } /** - * Session ID to close. + * Closes a session: emits shutdown, flushes pending events, releases the in-use lock, and disposes the active session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -217,7 +217,7 @@ public CompletableFuture close(SessionsCloseParams params) { } /** - * Session IDs to close, deactivate, and delete from disk. + * Closes, deactivates, and deletes a set of sessions, returning the bytes freed per session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -228,18 +228,18 @@ public CompletableFuture bulkDelete(SessionsBulkDelete } /** - * Session ID to delete from disk. + * Deletes one local session from disk after running the same lifecycle hooks as the session manager. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture delete(SessionsDeleteParams params) { + CompletableFuture delete(SessionsDeleteParams params) { return caller.invoke("sessions.delete", params, Void.class); } /** - * Age threshold and optional flags controlling which old sessions are pruned (or simulated when dryRun is true). + * Deletes sessions older than the given threshold, with optional dry-run and exclusion list. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -250,7 +250,7 @@ public CompletableFuture pruneOld(SessionsPruneOldParams } /** - * Session ID whose pending events should be flushed to disk. + * Flushes a session's pending events to disk. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -261,7 +261,7 @@ public CompletableFuture save(SessionsSaveParams params) { } /** - * Session ID whose in-use lock should be released. + * Releases the in-use lock held by this process for a session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -272,7 +272,7 @@ public CompletableFuture releaseLock(SessionsReleaseLockParams params) { } /** - * Session metadata records to enrich with summary and context information. + * Backfills missing summary and context fields on the supplied session metadata records. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -283,7 +283,7 @@ public CompletableFuture enrichMetadata(SessionsEn } /** - * Active session ID and an optional flag for deferring repo-level hooks until folder trust. + * Reloads user, plugin, and (optionally) repo hooks on the active session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -294,7 +294,7 @@ public CompletableFuture reloadPluginHooks(SessionsReloadPluginHooksParams } /** - * Active session ID whose deferred repo-level hooks should be loaded. + * Loads previously-deferred repo-level hooks on the active session, returning queued startup prompts. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -305,7 +305,7 @@ public CompletableFuture loadDeferredRepoHo } /** - * Manager-wide additional plugins to register; replaces any previously-configured set. + * Replaces the manager-wide additional plugins registered with the session manager. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -316,18 +316,18 @@ public CompletableFuture setAdditionalPlugins(SessionsSetAdditionalPlugins } /** - * Session ID whose board entry count should be returned. + * Gets the dynamic-context board entry count associated with a session, when available. Internal: this exists solely so CLI telemetry events (`rem_spawn_gate`, `rem_consolidation_complete`) can pair START / END board counts around the detached rem-agent spawn. "Dynamic context board" is a runtime-internal concept that is not part of the public SDK contract; the long-term plan is to relocate the telemetry emission into the runtime so this method can be deleted entirely. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture getBoardEntryCount(SessionsGetBoardEntryCountParams params) { + CompletableFuture getBoardEntryCount(SessionsGetBoardEntryCountParams params) { return caller.invoke("sessions.getBoardEntryCount", params, SessionsGetBoardEntryCountResult.class); } /** - * Parameters for attaching the remote-control singleton to a session. + * Attaches the runtime-managed remote-control singleton to a session, awaiting initial setup. If remote control is already attached to a different session, the singleton is transferred (preserving the underlying Mission Control connection). Returns the final status. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -338,7 +338,7 @@ public CompletableFuture startRemoteControl(Se } /** - * Parameters for atomically rebinding the remote-control singleton. + * Atomically rebinds the remote-control singleton to a different session, preserving the underlying Mission Control connection. When `expectedFromSessionId` is provided and does not match the singleton's current `attachedSessionId`, the transfer is rejected with `transferred: false` and the current status is returned unchanged. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -349,7 +349,7 @@ public CompletableFuture transferRemoteCont } /** - * Patch for the singleton's steering state. + * Patches the steering state of the active remote-control singleton. When remote control is off, this is a no-op and the off status is returned. Today only `enabled: true` is actionable on the underlying exporter; passing `false` is reserved for future use. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -360,7 +360,7 @@ public CompletableFuture setRemoteContro } /** - * Parameters for stopping the remote-control singleton. + * Stops the remote-control singleton. When `expectedSessionId` is provided and does not match the singleton's current `attachedSessionId`, the stop is rejected with `stopped: false` and the current status is returned unchanged (unless `force` is set, in which case the singleton is unconditionally torn down). *

* Invokes the method with no params, applying the runtime defaults. * @@ -373,7 +373,7 @@ public CompletableFuture stopRemoteControl() { } /** - * Parameters for stopping the remote-control singleton. + * Stops the remote-control singleton. When `expectedSessionId` is provided and does not match the singleton's current `attachedSessionId`, the stop is rejected with `stopped: false` and the current status is returned unchanged (unless `force` is set, in which case the singleton is unconditionally torn down). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -384,7 +384,7 @@ public CompletableFuture stopRemoteControl(Sess } /** - * Wrapper for the singleton's current status. + * Returns the current state of the remote-control singleton, including the attached session id and frontend URL when active. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -395,13 +395,13 @@ public CompletableFuture getRemoteControlS } /** - * Params to attach or detach an in-process ExtensionController delegate. + * Attaches (or detaches) an in-process ExtensionController delegate for the given session in a local host adapter. Pass `controller: undefined` to detach. Internal because the controller cannot cross the JSON-RPC boundary; the runtime manages its own session extension service. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture configureSessionExtensions(SessionsConfigureSessionExtensionsParams params) { + CompletableFuture configureSessionExtensions(SessionsConfigureSessionExtensionsParams params) { return caller.invoke("sessions.configureSessionExtensions", params, Void.class); } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsApi.java index 2f77e8da88..30b35346bb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsApi.java @@ -34,7 +34,7 @@ public final class ServerSkillsApi { } /** - * Side-effect-free planning of one verified Agent Finder Skill candidate. + * Plans installation of a verified Agent Finder Skill candidate without writing files. The returned review is safe to present to a user and installing always leaves the Skill disabled until separately enabled. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -45,7 +45,7 @@ public CompletableFuture planInstall(SkillsPl } /** - * Applies exactly one retained verified Skill installation plan. + * Consumes one verified Skill installation plan, requests explicit human consent through installations.confirm on the original connection, then revalidates and installs the Skill disabled. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -56,7 +56,7 @@ public CompletableFuture applyInstall(SkillsApplyInstal } /** - * Read-only preparation of one owned Skill removal under fresh selected-session authority. + * Prepares a read-only removal plan for an owned verified Agent Finder Skill installation. Uninstall planning is never gated by the Skill-install feature flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -67,7 +67,7 @@ public CompletableFuture planUninstall(Skills } /** - * One-use application of the exact retained Skill removal plan. + * Consumes an owned Skill removal plan, requests explicit human consent through installations.confirm, refuses drift, and removes the exact owned files through quarantine. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -78,7 +78,7 @@ public CompletableFuture applyUninstall(SkillsApplyUnin } /** - * Optional project paths and additional skill directories to include in discovery. + * Discovers skills across global and project sources. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -89,7 +89,7 @@ public CompletableFuture discover(SkillsDiscoverParams par } /** - * Optional project paths to enumerate. + * Returns the canonical directories where a client may create skills that the runtime will recognize, including ones that do not exist yet. Project directories become active once created. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsConfigApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsConfigApi.java index c29770bf14..35cf2e2ef1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsConfigApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsConfigApi.java @@ -27,7 +27,7 @@ public final class ServerSkillsConfigApi { } /** - * Skill names to mark as disabled in global configuration, replacing any previous list. + * Replaces the global list of disabled skills. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture setDisabledSkills(SkillsConfigSetDisabledSkillsPa } /** - * Adds or removes a single skill from the global disabled list, leaving every other entry untouched. + * Atomically adds or removes one skill from the disabled list. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsInstallationsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsInstallationsApi.java index e0d0afe72a..51029bc363 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsInstallationsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSkillsInstallationsApi.java @@ -27,7 +27,7 @@ public final class ServerSkillsInstallationsApi { } /** - * Inventory request under an explicitly selected existing session. + * Lists owned verified Agent Finder Skill installations for the selected existing session. Listing is never gated by the Skill-install feature flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture list(SkillsInstallat } /** - * Inventory request under an explicitly selected existing session. + * Reconciles interrupted owned Skill installation work for the selected existing session, then inspects owned inventory. Recovery is never gated by the Skill-install feature flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture recover(SkillsInstal } /** - * Existing-operation control. A new session selector is deliberately not accepted. + * Inspects a known Skill installation operation on its original runtime connection. Status is never gated by the Skill-install feature flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ public CompletableFuture status(SkillsInstall } /** - * Existing-operation control. A new session selector is deliberately not accepted. + * Requests cancellation of a known Skill installation operation before commit. Already-started durable work requires recovery instead of silent replay. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -71,7 +71,7 @@ public CompletableFuture cancel(SkillsInstall } /** - * Persisted enablement update for one owned Skill installation. + * Atomically persists enablement for one owned Agent Finder Skill and reconciles the selected bound session. Enablement is installation-scoped by receipt identity and is never gated by the Skill-install feature flag. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerToolsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerToolsApi.java index 2938010010..6f3b3b8759 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerToolsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerToolsApi.java @@ -27,7 +27,7 @@ public final class ServerToolsApi { } /** - * Optional model identifier whose tool overrides should be applied to the listing. + * Lists built-in tools available for a model. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerUserSettingsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerUserSettingsApi.java index 665cfb107a..d46e570346 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerUserSettingsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerUserSettingsApi.java @@ -27,7 +27,7 @@ public final class ServerUserSettingsApi { } /** - * Invokes {@code user.settings.reload}. + * Drops this runtime process's in-memory user settings cache so the next settings read observes disk. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -38,7 +38,7 @@ public CompletableFuture reload() { } /** - * 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. + * 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. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture get() { } /** - * 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. + * 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. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsApi.java index 76b3fdc7e0..2b4f752fdf 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsApi.java @@ -35,7 +35,7 @@ public final class SessionAccountsApi { } /** - * Enumerate request carrying the typed collection query. + * Enumerate a typed accounts collection: the signed-in accounts, or the providers offered for interactive login. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -51,7 +51,7 @@ public CompletableFuture enumerate(SessionAccountsEnumerateP } /** - * Read request carrying the typed datum query. + * Read one typed accounts datum: the active account, a neutral status summary, or the last authentication errors. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -67,7 +67,7 @@ public CompletableFuture get(SessionAccountsGetParams params) { } /** - * Mutation request carrying the typed write command. + * Apply one non-interactive accounts mutation: switch the active account, log an account out, or set credentials from a token. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginApi.java index 5537a0dad2..24e3c6a642 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAccountsLoginApi.java @@ -31,7 +31,7 @@ public final class SessionAccountsLoginApi { } /** - * Begin an interactive login flow for a provider kind. Dispatch is kind-only. + * Begin an interactive login flow for a provider kind (dispatch is kind-only) and return its opaque flow id and first step. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture begin(SessionAccountsL } /** - * Advance an in-flight login flow, optionally fulfilling an input-required step. + * Advance an in-flight login flow, optionally fulfilling an input-required step, and return the next step. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture advance(SessionAccountsLoginAdvanceParam } /** - * Cancel an in-flight login flow. + * Cancel an in-flight login flow and release its resources. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAgentApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAgentApi.java index d2499fe3a5..0030c622d0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAgentApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAgentApi.java @@ -31,7 +31,7 @@ public final class SessionAgentApi { } /** - * Controls whether built-in agents and authored prompt text are included. + * Lists agents available to the session. Defaults to custom agents only; pass includeBuiltInAgents to include the effective built-in agents. *

* Invokes the method with no params, applying the runtime defaults. * @@ -44,7 +44,7 @@ public CompletableFuture list() { } /** - * Controls whether built-in agents and authored prompt text are included. + * Lists agents available to the session. Defaults to custom agents only; pass includeBuiltInAgents to include the effective built-in agents. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -60,7 +60,7 @@ public CompletableFuture list(SessionAgentListParams par } /** - * An in-memory authored prompt override for an available agent. + * Sets an in-memory authored prompt override for an available agent. For built-in agents, this replaces only the static base prompt while preserving runtime-owned dynamic prompt composition and behavior. The special `general-purpose` agent is not overrideable. Overrides are not persisted; resumed and forked sessions start without them, so the host must re-apply them. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture setPrompt(SessionAgentSetPromptParams params) { } /** - * Identifies the target session. + * Gets the currently selected custom agent for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -87,7 +87,7 @@ public CompletableFuture getCurrent() { } /** - * Name of the custom agent to select for subsequent turns. + * Selects a custom agent for subsequent turns in the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -103,7 +103,7 @@ public CompletableFuture select(SessionAgentSelectPara } /** - * Identifies the target session. + * Clears the selected custom agent and returns the session to the default agent. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -114,7 +114,7 @@ public CompletableFuture deselect() { } /** - * Identifies the target session. + * Reloads custom agent definitions and returns the refreshed list. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java index ab4058e6b6..8edc125d7e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java @@ -29,7 +29,7 @@ public final class SessionAutopilotObjectiveApi { } /** - * Identifies the target session. + * Reads the current canonical autopilot objective state for this session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasActionApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasActionApi.java index 8c6ce4b79e..3e41100db6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasActionApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasActionApi.java @@ -31,7 +31,7 @@ public final class SessionCanvasActionApi { } /** - * Canvas action invocation parameters. + * Invokes an action on an open canvas instance. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasApi.java index e7cfc4be2f..fdcee24883 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasApi.java @@ -27,7 +27,7 @@ public final class SessionCanvasApi { /** API methods for the {@code canvas.action} sub-namespace. */ public final SessionCanvasActionApi action; /** API methods for the {@code canvas.provider} sub-namespace. */ - public final SessionCanvasProviderApi provider; + final SessionCanvasProviderApi provider; /** @param caller the RPC transport function */ SessionCanvasApi(RpcCaller caller, String sessionId) { @@ -38,7 +38,7 @@ public final class SessionCanvasApi { } /** - * Identifies the target session. + * Lists canvases declared for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -49,7 +49,7 @@ public CompletableFuture list() { } /** - * Identifies the target session. + * Lists currently open canvas instances for the live session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -60,7 +60,7 @@ public CompletableFuture listOpen() { } /** - * Canvas open parameters. + * Opens or focuses a canvas instance. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture open(SessionCanvasOpenParams p } /** - * Canvas close parameters. + * Closes an open canvas instance. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderApi.java index 95fc7ca660..90a80dfce0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderApi.java @@ -17,7 +17,7 @@ * @since 1.0.0 */ @javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class SessionCanvasProviderApi { +final class SessionCanvasProviderApi { private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; @@ -31,7 +31,7 @@ public final class SessionCanvasProviderApi { } /** - * Internal canvas provider registration parameters. + * Registers an internal canvas provider connection and its contributions. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -40,14 +40,14 @@ public final class SessionCanvasProviderApi { * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture register(SessionCanvasProviderRegisterParams params) { + CompletableFuture register(SessionCanvasProviderRegisterParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); return caller.invoke("session.canvas.provider.register", _p, Void.class); } /** - * Internal canvas provider unregistration parameters. + * Unregisters an internal canvas provider connection. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -56,7 +56,7 @@ public CompletableFuture register(SessionCanvasProviderRegisterParams para * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture unregister(SessionCanvasProviderUnregisterParams params) { + CompletableFuture unregister(SessionCanvasProviderUnregisterParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); return caller.invoke("session.canvas.provider.unregister", _p, Void.class); diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderRegisterParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderRegisterParams.java index 3047576670..a9213671aa 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderRegisterParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderRegisterParams.java @@ -24,7 +24,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionCanvasProviderRegisterParams( +record SessionCanvasProviderRegisterParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Connection identifier for callback routing */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderUnregisterParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderUnregisterParams.java index f7b637ac65..4fc7273603 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderUnregisterParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCanvasProviderUnregisterParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionCanvasProviderUnregisterParams( +record SessionCanvasProviderUnregisterParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Connection identifier to unregister */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsApi.java index d2ef6a8f5c..55cd139ae5 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsApi.java @@ -31,7 +31,7 @@ public final class SessionCommandsApi { } /** - * Optional filters controlling which command sources to include in the listing. + * Lists slash commands available in the session. *

* Invokes the method with no params, applying the runtime defaults. * @@ -44,7 +44,7 @@ public CompletableFuture list() { } /** - * Optional filters controlling which command sources to include in the listing. + * Lists slash commands available in the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -60,7 +60,7 @@ public CompletableFuture list(SessionCommandsListPara } /** - * Slash command name and optional raw input string to invoke. + * Invokes a slash command in the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture invoke(SessionCommandsInv } /** - * The pending slash-command invocation effect to finalize, plus whether the host applied or cancelled it. + * Finalizes persistence associated with a client-applied slash-command effect. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -85,14 +85,14 @@ public CompletableFuture invoke(SessionCommandsInv * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture finalizeInvocationEffect(SessionCommandsFinalizeInvocationEffectParams params) { + CompletableFuture finalizeInvocationEffect(SessionCommandsFinalizeInvocationEffectParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.commands.finalizeInvocationEffect", _p, SessionCommandsFinalizeInvocationEffectResult.class); + return caller.invoke("session.commands.finalizeInvocationEffect", _p, CommandsFinalizeInvocationEffectResult.class); } /** - * Pending command request ID and an optional error if the client handler failed. + * Reports completion of a pending client-handled slash command. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -108,7 +108,7 @@ public CompletableFuture handlePendin } /** - * Slash command name and argument string to execute synchronously. + * Executes a slash command synchronously and returns any error. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -124,7 +124,7 @@ public CompletableFuture execute(SessionCommandsEx } /** - * Slash-prefixed command string to enqueue for FIFO processing. + * Enqueues a slash command for FIFO processing on the local session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -140,7 +140,7 @@ public CompletableFuture enqueue(SessionCommandsEnqueuePar } /** - * Queued-command request ID and the result indicating whether the host executed it (and whether to stop processing further queued commands). + * Reports whether the host actually executed a queued command and whether to continue processing. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsFinalizeInvocationEffectParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsFinalizeInvocationEffectParams.java index 2a1b75f5b8..337489704a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsFinalizeInvocationEffectParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCommandsFinalizeInvocationEffectParams.java @@ -24,7 +24,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionCommandsFinalizeInvocationEffectParams( +record SessionCommandsFinalizeInvocationEffectParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** The slash-command result object that produced the pending effect, echoed back unchanged. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCompletionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCompletionsApi.java index 6b9d8aa252..f9fa5d1dae 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCompletionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCompletionsApi.java @@ -31,7 +31,7 @@ public final class SessionCompletionsApi { } /** - * Identifies the target session. + * Gets the characters that should trigger host-driven completions for the session. Empty disables host-driven completions (e.g. local sessions, or a relay host that does not advertise them). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture getTrigge } /** - * Request host-driven completions for the current composer input. + * Requests host-driven completion items for the current composer input. Returns an empty list when the host has no items or does not support completions. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsApi.java index 1551b86b71..e69fc59d23 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsApi.java @@ -8,6 +8,7 @@ package com.github.copilot.generated.rpc; import com.github.copilot.CopilotExperimental; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import javax.annotation.processing.Generated; @@ -31,7 +32,7 @@ public final class SessionConnectorsApi { } /** - * Identifies the target session. + * Returns feature availability and bounded polling limits for the EXPERIMENTAL session connector API. This method never performs a Connector service request. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +43,22 @@ public CompletableFuture getCapabilities } /** - * Identifies the target session. + * Returns the session account selection, or null. + * + * @return a future that completes with the {@code ConnectorSessionAccount} value, + * or {@code null} when the result is absent. Callers must handle the + * {@code null} completion value. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture getAccount() { + return caller.invoke("session.connectors.getAccount", java.util.Map.of("sessionId", this.sessionId), ConnectorSessionAccount.class); + } + + /** + * Returns authoritative session Connector state from current availability, pinned account selection, cached catalog, and live MCP projection without performing a Connector service request. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -53,7 +69,7 @@ public CompletableFuture getStatus() { } /** - * Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + * Returns the cached Connector catalog for the pinned opaque account selection, fetching it only when this session has no cached catalog. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -69,7 +85,7 @@ public CompletableFuture list(SessionConnectorsList } /** - * Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + * Refreshes and validates the Connector catalog for the pinned opaque account selection. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -85,7 +101,7 @@ public CompletableFuture refresh(SessionConnecto } /** - * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + * Initiates an idempotent Connector connection request without opening a browser. Returns connected when the service is immediately authoritative, consent_required with a validated URL, or pending with an opaque continuation ID. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -101,7 +117,7 @@ public CompletableFuture connect(SessionConnectorsConnec } /** - * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + * Re-initiates an idempotent Connector connection request without browser or UI effects, with the same typed outcomes as connect. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -117,7 +133,7 @@ public CompletableFuture reconnect(SessionConnectorsReco } /** - * Explicitly bounded continuation of a pending Connector connection. + * Continues a pending Connector connection with caller-supplied attempt, interval, and deadline bounds. The runtime never opens the returned consent URL. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -133,7 +149,7 @@ public CompletableFuture continueConnection(SessionConne } /** - * Selects one Connector and the pinned host-owned account used for its service and MCP authorization. + * Disconnects one Connector for the pinned opaque account selection, refreshes the authoritative catalog, and removes its session-owned MCP projection. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -149,7 +165,7 @@ public CompletableFuture disconnect(SessionCo } /** - * Requests authoritative Connector-to-MCP reconciliation for the pinned account. + * Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -165,7 +181,22 @@ public CompletableFuture reconcile(SessionConn } /** - * Pins a Connector operation to one host-owned GitHub account through its opaque selection ID. Provider tokens are never accepted. + * Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status. + *

+ * Accepts the extensible request, including inputs added after the params record. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture reconcile(SessionConnectorsReconcileRequest request) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(Objects.requireNonNull(request, "request")); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.connectors.reconcile", _p, SessionConnectorsReconcileResult.class); + } + + /** + * Reconciles the authoritative Connector catalog into the session MCP projection during startup with a bounded deadline and fail-closed cleanup. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -174,21 +205,21 @@ public CompletableFuture reconcile(SessionConn * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture reconcileForStartup(SessionConnectorsReconcileForStartupParams params) { + CompletableFuture reconcileForStartup(SessionConnectorsReconcileForStartupParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.connectors.reconcileForStartup", _p, SessionConnectorsReconcileForStartupResult.class); + return caller.invoke("session.connectors.reconcileForStartup", _p, ConnectorStatus.class); } /** - * Identifies the target session. + * Removes the runtime-owned Connector MCP projection without changing service-side connections. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture withdrawProjection() { - return caller.invoke("session.connectors.withdrawProjection", java.util.Map.of("sessionId", this.sessionId), SessionConnectorsWithdrawProjectionResult.class); + CompletableFuture withdrawProjection() { + return caller.invoke("session.connectors.withdrawProjection", java.util.Map.of("sessionId", this.sessionId), ConnectorStatus.class); } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpConfigureGitHubResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetAccountParams.java similarity index 83% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpConfigureGitHubResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetAccountParams.java index a22656359f..30288a8081 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpConfigureGitHubResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetAccountParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Result of configuring GitHub MCP. + * Identifies the target session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -23,8 +23,8 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionMcpConfigureGitHubResult( - /** Whether GitHub MCP configuration changed. */ - @JsonProperty("changed") Boolean changed +public record SessionConnectorsGetAccountParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetCapabilitiesResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetCapabilitiesResult.java index 65f41baece..f82d8b44c2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetCapabilitiesResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsGetCapabilitiesResult.java @@ -32,6 +32,10 @@ public record SessionConnectorsGetCapabilitiesResult( @JsonProperty("consentContinuation") Boolean consentContinuation, /** Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. */ @JsonProperty("opaqueAccountSelection") Boolean opaqueAccountSelection, + /** Whether getAccount is supported. Absence means false. */ + @JsonProperty("sessionAccountSelection") Boolean sessionAccountSelection, + /** Whether reconcile accepts forceConnectorName. Absence means false. */ + @JsonProperty("targetedReconcile") Boolean targetedReconcile, /** Maximum accepted polling attempts for one continuation call. */ @JsonProperty("maxPollAttempts") Long maxPollAttempts, /** Maximum accepted delay in milliseconds between polling attempts. */ @@ -39,4 +43,27 @@ public record SessionConnectorsGetCapabilitiesResult( /** Maximum accepted wall-clock deadline in milliseconds for one continuation call. */ @JsonProperty("maxDeadlineMs") Long maxDeadlineMs ) { + + /** + * Creates a record with the components it had before later optional fields were added. + * + * @param apiVersion Connector API contract version. + * @param availability Current session availability. Disabled availability is reported without making a Connector request. + * @param consentContinuation Whether connect and reconnect can return an opaque continuation for bounded consent polling. + * @param opaqueAccountSelection Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. + * @param maxPollAttempts Maximum accepted polling attempts for one continuation call. + * @param maxPollIntervalMs Maximum accepted delay in milliseconds between polling attempts. + * @param maxDeadlineMs Maximum accepted wall-clock deadline in milliseconds for one continuation call. + */ + public SessionConnectorsGetCapabilitiesResult( + Long apiVersion, + ConnectorAvailability availability, + Boolean consentContinuation, + Boolean opaqueAccountSelection, + Long maxPollAttempts, + Long maxPollIntervalMs, + Long maxDeadlineMs + ) { + this(apiVersion, availability, consentContinuation, opaqueAccountSelection, null, null, maxPollAttempts, maxPollIntervalMs, maxDeadlineMs); + } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileForStartupParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileForStartupParams.java index 194ee97ecf..3cd95a487c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileForStartupParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileForStartupParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionConnectorsReconcileForStartupParams( +record SessionConnectorsReconcileForStartupParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Opaque account selection ID previously returned by an account discovery API. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileForStartupResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileForStartupResult.java deleted file mode 100644 index fa563d54d5..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileForStartupResult.java +++ /dev/null @@ -1,43 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; -import java.util.List; -import javax.annotation.processing.Generated; - -/** - * Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ -@CopilotExperimental -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record SessionConnectorsReconcileForStartupResult( - /** Connector API contract version. */ - @JsonProperty("apiVersion") Long apiVersion, - /** Current feature and session availability. */ - @JsonProperty("availability") ConnectorAvailability availability, - /** Opaque account selection pinned to this session, when one has been selected. */ - @JsonProperty("accountId") String accountId, - /** Latest validated catalog snapshot, when available. */ - @JsonProperty("catalog") ConnectorCatalogResult catalog, - /** Exact selected account and stable scope requiring an authorization update, when proven by the Connector service. */ - @JsonProperty("authorizationRequirement") ConnectorAuthorizationRequirement authorizationRequirement, - /** Live MCP status for every Connector-owned runtime server. */ - @JsonProperty("runtimeServers") List runtimeServers, - /** Number of active opaque connection continuations. */ - @JsonProperty("pendingConnections") Long pendingConnections -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileRequest.java new file mode 100644 index 0000000000..cf25537c6e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsReconcileRequest.java @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.Objects; +import javax.annotation.processing.Generated; + +/** + * Requests authoritative Connector-to-MCP reconciliation for the pinned account. + *

+ * Required inputs are constructor arguments. Optional inputs have fluent setters. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class SessionConnectorsReconcileRequest { + + /** Opaque account selection ID. It must match the account already pinned to the session, if any. */ + @JsonProperty("accountId") + private final String accountId; + + /** When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. */ + @JsonProperty("refreshCatalog") + private Boolean refreshCatalog; + + /** Optional Connector name to reinitialize. Requires the targetedReconcile capability. */ + @JsonProperty("forceConnectorName") + private String forceConnectorName; + + /** + * Creates a request with its required inputs. + * + * @param accountId Opaque account selection ID. It must match the account already pinned to the session, if any. + */ + public SessionConnectorsReconcileRequest(String accountId) { + this.accountId = Objects.requireNonNull(accountId, "accountId"); + } + + /** + * Returns the {@code accountId} property. + * + * @return Opaque account selection ID. It must match the account already pinned to the session, if any. + */ + public String getAccountId() { + return accountId; + } + + /** + * Returns the {@code refreshCatalog} property. + * + * @return When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. + */ + public Boolean getRefreshCatalog() { + return refreshCatalog; + } + + /** + * Returns the {@code forceConnectorName} property. + * + * @return Optional Connector name to reinitialize. Requires the targetedReconcile capability. + */ + public String getForceConnectorName() { + return forceConnectorName; + } + + /** + * Sets the {@code refreshCatalog} property. + * + * @param value When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. + * @return this request + */ + public SessionConnectorsReconcileRequest setRefreshCatalog(Boolean value) { + this.refreshCatalog = value; + return this; + } + + /** + * Sets the {@code forceConnectorName} property. + * + * @param value Optional Connector name to reinitialize. Requires the targetedReconcile capability. + * @return this request + */ + public SessionConnectorsReconcileRequest setForceConnectorName(String value) { + this.forceConnectorName = value; + return this; + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsWithdrawProjectionParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsWithdrawProjectionParams.java index 2e534c1979..66461d320f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsWithdrawProjectionParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsWithdrawProjectionParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionConnectorsWithdrawProjectionParams( +record SessionConnectorsWithdrawProjectionParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsWithdrawProjectionResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsWithdrawProjectionResult.java deleted file mode 100644 index 1ae332cab0..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionConnectorsWithdrawProjectionResult.java +++ /dev/null @@ -1,43 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; -import java.util.List; -import javax.annotation.processing.Generated; - -/** - * Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ -@CopilotExperimental -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record SessionConnectorsWithdrawProjectionResult( - /** Connector API contract version. */ - @JsonProperty("apiVersion") Long apiVersion, - /** Current feature and session availability. */ - @JsonProperty("availability") ConnectorAvailability availability, - /** Opaque account selection pinned to this session, when one has been selected. */ - @JsonProperty("accountId") String accountId, - /** Latest validated catalog snapshot, when available. */ - @JsonProperty("catalog") ConnectorCatalogResult catalog, - /** Exact selected account and stable scope requiring an authorization update, when proven by the Connector service. */ - @JsonProperty("authorizationRequirement") ConnectorAuthorizationRequirement authorizationRequirement, - /** Live MCP status for every Connector-owned runtime server. */ - @JsonProperty("runtimeServers") List runtimeServers, - /** Number of active opaque connection continuations. */ - @JsonProperty("pendingConnections") Long pendingConnections -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionContentExclusionApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionContentExclusionApi.java index eb621e6b49..afddaed971 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionContentExclusionApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionContentExclusionApi.java @@ -31,7 +31,7 @@ public final class SessionContentExclusionApi { } /** - * Local file system absolute paths within the session working directory to check against its content-exclusion policy. + * Checks local file system absolute paths within the session working directory against its content-exclusion policy. Results preserve input order. Unsupported paths/filesystems and unavailable policy evaluation return available false, and callers must treat every requested path as excluded. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsApi.java index 18d27db9b5..b0183065b0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsApi.java @@ -29,14 +29,14 @@ public final class SessionCustomizationsApi { } /** - * Identifies the target session. + * 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. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture reload() { - return caller.invoke("session.customizations.reload", java.util.Map.of("sessionId", this.sessionId), SessionCustomizationsReloadResult.class); + public CompletableFuture reload() { + return caller.invoke("session.customizations.reload", java.util.Map.of("sessionId", this.sessionId), CustomizationsReloadResult.class); } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDebugApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDebugApi.java index b2cd030183..ed06730c49 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDebugApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDebugApi.java @@ -31,7 +31,7 @@ public final class SessionDebugApi { } /** - * Options for collecting a session debug bundle with configurable redaction. + * Collects a session debug log bundle into a local archive or staging directory. Logs are redacted by default; redaction can be configured per caller-provided diagnostic entry. The runtime includes session-owned logs by default and accepts caller-provided diagnostic entries so host applications can add their own files without changing this API shape. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDiagnosticsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDiagnosticsApi.java index af4c57837b..d263853aec 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDiagnosticsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionDiagnosticsApi.java @@ -31,7 +31,7 @@ public final class SessionDiagnosticsApi { } /** - * Patch session diagnostic thresholds for explicitly supplied sources. + * Patches configured session diagnostic sources without restarting their producers. Setting a source level to off clears its retained diagnostics and invalidates cursors selecting that source. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture configure(SessionDia } /** - * Cursor-based request for session diagnostics. The default limit is 100 (maximum 500); the default waitMs is zero (maximum 30000). + * Reads a bounded batch of retained session diagnostics for the selected sources. Records are never consumed and each reader advances independently through its opaque cursor. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionEventLogApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionEventLogApi.java index 8ad9b3b1f5..87ad200719 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionEventLogApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionEventLogApi.java @@ -31,7 +31,7 @@ public final class SessionEventLogApi { } /** - * Cursor, batch size, and optional long-poll/filter parameters for reading session events. + * Reads a batch of session events from a cursor, optionally waiting for new events. Supports tail-first reads via `direction: backward`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture read(SessionEventLogReadPara } /** - * Identifies the target session. + * Returns a snapshot of the current tail cursor without consuming events. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -58,7 +58,7 @@ public CompletableFuture tail() { } /** - * Event type to register consumer interest for, used by runtime gating logic. + * Registers consumer interest in an event type for runtime gating purposes. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture registerInterest } /** - * Opaque handle previously returned by `registerInterest` to release. + * Releases a consumer's previously-registered interest in an event type. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsApi.java index e21c109684..cc1de448bf 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionExtensionsApi.java @@ -31,7 +31,7 @@ public final class SessionExtensionsApi { } /** - * Identifies the target session. + * Lists extensions discovered for the session and their current status. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture list() { } /** - * Source-qualified extension identifier to enable for the session. + * Enables an extension for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture enable(SessionExtensionsEnableParams params) { } /** - * Source-qualified extension identifier to disable for the session. + * Disables an extension for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture disable(SessionExtensionsDisableParams params) { } /** - * Identifies the target session. + * Reloads extension definitions and processes for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -85,7 +85,7 @@ public CompletableFuture reload() { } /** - * Parameters for session.extensions.sendAttachmentsToMessage. + * Push attachments into the next user-message turn from an extension. The host should surface them as composer pills and forward them via the next session.send call. Callable only by extension-owned connections. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFleetApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFleetApi.java index ca855b70a9..1c5730cd88 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFleetApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFleetApi.java @@ -31,7 +31,7 @@ public final class SessionFleetApi { } /** - * Parameters for starting fleet orchestration: an optional user prompt combined with the fleet instructions, plus the send options forwarded to the resulting turn. + * Starts fleet mode by submitting the fleet orchestration prompt to the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsError.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsError.java index 349114dfde..0170b7f3b0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsError.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsError.java @@ -24,6 +24,8 @@ public record SessionFsError( /** Error classification */ @JsonProperty("code") SessionFsErrorCode code, /** Free-form detail about the error, for logging/diagnostics */ - @JsonProperty("message") String message + @JsonProperty("message") String message, + /** For failed writeFile requests only: true if the provider changed the target before failing. Omit when unknown or unchanged. */ + @JsonProperty("writeChanged") Boolean writeChanged ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostRegisterSessionResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsReadFileBytesParams.java similarity index 77% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostRegisterSessionResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsReadFileBytesParams.java index 111c45a1db..9e89b0c1a2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HostRegisterSessionResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsReadFileBytesParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * The existing runtime identity and its resource on the listener. + * Path of the binary file to read from the client-provided session filesystem. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -23,10 +23,10 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record HostRegisterSessionResult( - /** Canonical runtime ID of the published session. */ +public record SessionFsReadFileBytesParams( + /** Target session identifier */ @JsonProperty("sessionId") String sessionId, - /** AHP resource URI for the session on this listener. */ - @JsonProperty("sessionUri") String sessionUri + /** Path using SessionFs conventions */ + @JsonProperty("path") String path ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsReadFileBytesResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsReadFileBytesResult.java new file mode 100644 index 0000000000..f70a78c72f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsReadFileBytesResult.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * File bytes as standard base64, or a filesystem error if the read failed. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsReadFileBytesResult( + /** Exact file bytes encoded as standard base64 */ + @JsonProperty("content") String content, + /** Describes a filesystem error. */ + @JsonProperty("error") SessionFsError error +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsSetProviderCapabilities.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsSetProviderCapabilities.java index 7d6c0adb39..f7d047c273 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsSetProviderCapabilities.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsSetProviderCapabilities.java @@ -22,6 +22,19 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record SessionFsSetProviderCapabilities( /** Whether the provider supports SQLite query/exists operations */ - @JsonProperty("sqlite") Boolean sqlite + @JsonProperty("sqlite") Boolean sqlite, + /** Whether the provider supports binary reads and writes through sessionFs.readFileBytes and sessionFs.writeFileBytes */ + @JsonProperty("binary") Boolean binary ) { + + /** + * Creates provider capabilities without binary reads. + * + * @param sqlite Whether the provider supports SQLite query/exists operations + */ + public SessionFsSetProviderCapabilities( + Boolean sqlite + ) { + this(sqlite, null); + } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsWriteFileBytesParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsWriteFileBytesParams.java new file mode 100644 index 0000000000..2d095f371d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFsWriteFileBytesParams.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionFsWriteFileBytesParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Path using SessionFs conventions */ + @JsonProperty("path") String path, + /** Exact file bytes encoded as standard base64 */ + @JsonProperty("content") String content, + /** Optional POSIX-style mode for newly created files */ + @JsonProperty("mode") Long mode +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthApi.java index 729979b5e2..388576154a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthApi.java @@ -32,7 +32,7 @@ public final class SessionGitHubAuthApi { } /** - * Identifies the target session. + * Gets authentication status and account metadata for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -43,7 +43,7 @@ public CompletableFuture getStatus() { } /** - * New auth credentials to install on the session. Omit to leave credentials unchanged. + * Updates the session's auth credentials used for outbound model and API requests. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -59,40 +59,48 @@ public CompletableFuture setCredentials(S } /** - * Identifies the target session. + * Gets the current authentication information for internal session hosts. + * + * @return a future that completes with the {@code AuthIdentity} value, + * or {@code null} when the result is absent. Callers must handle the + * {@code null} completion value. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture getCurrentAuthInfo() { - return caller.invoke("session.gitHubAuth.getCurrentAuthInfo", java.util.Map.of("sessionId", this.sessionId), Void.class); + CompletableFuture getCurrentAuthInfo() { + return caller.invoke("session.gitHubAuth.getCurrentAuthInfo", java.util.Map.of("sessionId", this.sessionId), AuthIdentity.class); } /** - * Identifies the target session. + * Gets all authentication accounts available to the internal session host. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture> getAllAuthAvailable() { + CompletableFuture> getAllAuthAvailable() { return caller.invoke("session.gitHubAuth.getAllAuthAvailable", java.util.Map.of("sessionId", this.sessionId), RpcMapper.INSTANCE.getTypeFactory().constructCollectionType(List.class, SessionAuthStatus.class)); } /** - * Identifies the target session. + * Refreshes Copilot account metadata for the current authentication. + * + * @return a future that completes with the {@code AuthIdentity} value, + * or {@code null} when the result is absent. Callers must handle the + * {@code null} completion value. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture refreshCopilotUser() { - return caller.invoke("session.gitHubAuth.refreshCopilotUser", java.util.Map.of("sessionId", this.sessionId), Void.class); + CompletableFuture refreshCopilotUser() { + return caller.invoke("session.gitHubAuth.refreshCopilotUser", java.util.Map.of("sessionId", this.sessionId), AuthIdentity.class); } /** - * Internal GitHub login parameters. + * Logs in a GitHub user through the internal session host. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -101,14 +109,14 @@ public CompletableFuture refreshCopilotUser() { * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture login(SessionGitHubAuthLoginParams params) { + CompletableFuture login(SessionGitHubAuthLoginParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); return caller.invoke("session.gitHubAuth.login", _p, AuthInfo.class); } /** - * Parameters for switching the session's active authentication. + * Switches the session to another available authentication. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -117,25 +125,25 @@ public CompletableFuture login(SessionGitHubAuthLoginParams params) { * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture switchToAuth(SessionGitHubAuthSwitchToAuthParams params) { + CompletableFuture switchToAuth(SessionGitHubAuthSwitchToAuthParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); return caller.invoke("session.gitHubAuth.switchToAuth", _p, Void.class); } /** - * Identifies the target session. + * Logs out the session's current GitHub authentication. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture logout() { + CompletableFuture logout() { return caller.invoke("session.gitHubAuth.logout", java.util.Map.of("sessionId", this.sessionId), Void.class); } /** - * Parameters identifying a GitHub authentication to log out. + * Logs out a specific GitHub authentication. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -144,20 +152,20 @@ public CompletableFuture logout() { * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture logoutUser(SessionGitHubAuthLogoutUserParams params) { + CompletableFuture logoutUser(SessionGitHubAuthLogoutUserParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); return caller.invoke("session.gitHubAuth.logoutUser", _p, Void.class); } /** - * Identifies the target session. + * Gets validation errors from the most recent authentication attempt. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture> lastAuthErrors() { + CompletableFuture> lastAuthErrors() { return caller.invoke("session.gitHubAuth.lastAuthErrors", java.util.Map.of("sessionId", this.sessionId), RpcMapper.INSTANCE.getTypeFactory().constructCollectionType(List.class, AuthValidationError.class)); } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthGetAllAuthAvailableParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthGetAllAuthAvailableParams.java index 446642d743..8ebe3b7519 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthGetAllAuthAvailableParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthGetAllAuthAvailableParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionGitHubAuthGetAllAuthAvailableParams( +record SessionGitHubAuthGetAllAuthAvailableParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthGetCurrentAuthInfoParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthGetCurrentAuthInfoParams.java index 88838f951d..24eb44e32c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthGetCurrentAuthInfoParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthGetCurrentAuthInfoParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionGitHubAuthGetCurrentAuthInfoParams( +record SessionGitHubAuthGetCurrentAuthInfoParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthLastAuthErrorsParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthLastAuthErrorsParams.java index d4fc156b28..02e7cec4f8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthLastAuthErrorsParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthLastAuthErrorsParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionGitHubAuthLastAuthErrorsParams( +record SessionGitHubAuthLastAuthErrorsParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthLoginParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthLoginParams.java index e0d777956f..6fde66966b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthLoginParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthLoginParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionGitHubAuthLoginParams( +record SessionGitHubAuthLoginParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** GitHub host URL */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthLogoutParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthLogoutParams.java index 172778d616..07283331b5 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthLogoutParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthLogoutParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionGitHubAuthLogoutParams( +record SessionGitHubAuthLogoutParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthLogoutUserParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthLogoutUserParams.java index 1aafec7811..c59fff894b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthLogoutUserParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthLogoutUserParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionGitHubAuthLogoutUserParams( +record SessionGitHubAuthLogoutUserParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Authentication information to log out */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthRefreshCopilotUserParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthRefreshCopilotUserParams.java index d0b9d33e56..3b0ee5f0a0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthRefreshCopilotUserParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthRefreshCopilotUserParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionGitHubAuthRefreshCopilotUserParams( +record SessionGitHubAuthRefreshCopilotUserParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthSwitchToAuthParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthSwitchToAuthParams.java index 7cd6f72b41..db357cab5c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthSwitchToAuthParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionGitHubAuthSwitchToAuthParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionGitHubAuthSwitchToAuthParams( +record SessionGitHubAuthSwitchToAuthParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Authentication information to activate */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionHistoryApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionHistoryApi.java index ad44d864de..ccfbfbd17f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionHistoryApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionHistoryApi.java @@ -31,7 +31,7 @@ public final class SessionHistoryApi { } /** - * Optional compaction parameters. + * Compacts the session history to reduce context usage. *

* Invokes the method with no params, applying the runtime defaults. * @@ -44,7 +44,7 @@ public CompletableFuture compact() { } /** - * Optional compaction parameters. + * Compacts the session history to reduce context usage. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -60,7 +60,7 @@ public CompletableFuture compact(SessionHistoryComp } /** - * Identifier of the event to truncate to; this event and all later events are removed. + * Truncates persisted session history to a specific event. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture truncate(SessionHistoryTr } /** - * Identifies the target session. + * Lists the user turns that the session can rewind to. Never rejects for a busy session: rewind reads need the session's file-change captures to be settled, so a session that still holds active work answers with `unavailableReason: "session-busy"` and no points, which the caller can retry. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -87,7 +87,7 @@ public CompletableFuture listRewindPoints( } /** - * Event boundary to preview for conversation-and-files rewind. + * Previews the files that a conversation-and-files rewind would restore. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -103,7 +103,7 @@ public CompletableFuture previewRewind(Sessio } /** - * Boundary and mode for rewinding session history. + * Rewinds the session conversation, optionally restoring files changed by the discarded turns. Not crash-atomic: file restore and conversation truncation are separate stores, applied in that order, so a process crash between them can leave the workspace rewound while the conversation still contains the discarded turns. There is no recovery journal; re-running the same rewind is the recovery path for a crash before truncation lands, since file restore is idempotent (already-restored files are reported as skipped) and truncation is re-derived from the still-retained boundary event. After truncation lands that boundary no longer exists, so the same request is rejected; the only stage that can still be outstanding is snapshot pruning, whose failure leaves orphan snapshots the capture store tolerates. The reverse inconsistency cannot occur, because truncation is never applied before file restore succeeds. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -119,7 +119,7 @@ public CompletableFuture rewind(SessionHistoryRewind } /** - * Identifies the target session. + * Cancels any in-progress background compaction on a local session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -130,7 +130,7 @@ public CompletableFuture cancelB } /** - * Identifies the target session. + * Aborts any in-progress manual compaction on a local session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -141,7 +141,7 @@ public CompletableFuture abortManualC } /** - * Identifies the target session. + * Produces a markdown summary of the session's conversation context for hand-off scenarios. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -152,7 +152,7 @@ public CompletableFuture summarizeForHa } /** - * Parameters for clearing the conversation and seeding the window that replaces it. + * Clears the session's conversation history, keeping only system and developer messages, and seeds the fresh context window with a first user message. Must be called from inside a tool handler: the clear has to drop the results of the tool calls its wipe orphans, and it rejects when no tool call is in flight. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionInstructionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionInstructionsApi.java index d5971f70fe..1252fa8f59 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionInstructionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionInstructionsApi.java @@ -29,7 +29,7 @@ public final class SessionInstructionsApi { } /** - * Identifies the target session. + * Gets instruction sources loaded for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -40,14 +40,14 @@ public CompletableFuture getSources() { } /** - * Identifies the target session. + * 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. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture reload() { - return caller.invoke("session.instructions.reload", java.util.Map.of("sessionId", this.sessionId), Void.class); + public CompletableFuture reload() { + return caller.invoke("session.instructions.reload", java.util.Map.of("sessionId", this.sessionId), SessionInstructionsGetSourcesResult.class); } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLimitPredictionApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLimitPredictionApi.java index a64f82afae..b28b54bd37 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLimitPredictionApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLimitPredictionApi.java @@ -31,7 +31,7 @@ public final class SessionLimitPredictionApi { } /** - * Parameters for predicting an AI-credit session limit. Omitting `modelId` uses the session's currently selected model. + * Predicts an AI-credit session limit for the session's resolved model. Returns an unavailable result instead of falling back when the current model is unresolved auto. *

* Invokes the method with no params, applying the runtime defaults. * @@ -44,7 +44,7 @@ public CompletableFuture predict() { } /** - * Parameters for predicting an AI-credit session limit. Omitting `modelId` uses the session's currently selected model. + * Predicts an AI-credit session limit for the session's resolved model. Returns an unavailable result instead of falling back when the current model is unresolved auto. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLspApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLspApi.java index c965007d43..2b20bd44e3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLspApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionLspApi.java @@ -31,7 +31,7 @@ public final class SessionLspApi { } /** - * Parameters for (re)loading the merged LSP configuration set. + * Loads the merged LSP configuration set for the session's working directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedSettingsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedSettingsApi.java index adf8568aa2..e2e4a67fe3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedSettingsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedSettingsApi.java @@ -29,7 +29,7 @@ public final class SessionManagedSettingsApi { } /** - * Identifies the target session. + * Waits for the live session's in-flight managed-settings application, then returns the retained effective snapshot used by runtime enforcement and by `session.managed_settings_resolved`. It does not perform another account, device, or server resolution, and rejects when resolution has not produced a snapshot. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedSettingsGetResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedSettingsGetResult.java index c432e26725..faf08c57d6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedSettingsGetResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionManagedSettingsGetResult.java @@ -15,7 +15,7 @@ import javax.annotation.processing.Generated; /** - * Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. + * Effective enterprise managed settings and contributing channels. Session events report applied policy; sessionless resolve reports an account/device snapshot, and compose reports a non-applying preview of candidate documents. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively. Session-local SDK-client policy is included only in session results. Marked experimental while the managed-settings surface stabilizes. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java index 4cb7a08312..aca53953b4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java @@ -33,6 +33,8 @@ public final class SessionMcpApi { public final SessionMcpAppsApi apps; /** API methods for the {@code mcp.resources} sub-namespace. */ public final SessionMcpResourcesApi resources; + /** API methods for the {@code mcp.prompts} sub-namespace. */ + public final SessionMcpPromptsApi prompts; /** @param caller the RPC transport function */ SessionMcpApi(RpcCaller caller, String sessionId) { @@ -42,10 +44,11 @@ public final class SessionMcpApi { this.headers = new SessionMcpHeadersApi(caller, sessionId); this.apps = new SessionMcpAppsApi(caller, sessionId); this.resources = new SessionMcpResourcesApi(caller, sessionId); + this.prompts = new SessionMcpPromptsApi(caller, sessionId); } /** - * Identifies the target session. + * Lists materialized MCP servers and their connection status. Cache misses may start and wait for MCP servers. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -56,7 +59,18 @@ public CompletableFuture list() { } /** - * Server name whose tool list should be returned. + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture listConfigured() { + return caller.invoke("session.mcp.listConfigured", java.util.Map.of("sessionId", this.sessionId), SessionMcpListConfiguredResult.class); + } + + /** + * 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. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -72,7 +86,7 @@ public CompletableFuture listTools(SessionMcpListTool } /** - * Name of the MCP server to enable for the session. + * Enables an MCP server for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -88,7 +102,7 @@ public CompletableFuture enable(SessionMcpEnableParams params) { } /** - * Name of the MCP server to enable for the session. + * Enables an MCP server for the session. *

* Accepts the extensible request, including inputs added after the params record. * @@ -103,7 +117,7 @@ public CompletableFuture enable(SessionMcpEnableRequest request) { } /** - * Name of the MCP server to disable for the session. + * Disables an MCP server for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -119,7 +133,7 @@ public CompletableFuture disable(SessionMcpDisableParams params) { } /** - * Name of the MCP server to disable for the session. + * Disables an MCP server for the session. *

* Accepts the extensible request, including inputs added after the params record. * @@ -134,7 +148,7 @@ public CompletableFuture disable(SessionMcpDisableRequest request) { } /** - * Identifies the target session. + * Reloads MCP server connections for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -145,7 +159,7 @@ public CompletableFuture reload() { } /** - * Identifies the target session. + * Releases any turns waiting on an in-flight MCP load without cancelling the load, letting the agent proceed while MCP servers finish connecting in the background. No-op when no MCP load is in flight or waiting turns were already released. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -156,7 +170,7 @@ public CompletableFuture moveLoadingToB } /** - * Opaque MCP reload configuration. + * Reloads MCP server connections for the session with an explicit host-provided configuration. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -165,14 +179,14 @@ public CompletableFuture moveLoadingToB * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture reloadWithConfig(SessionMcpReloadWithConfigParams params) { + CompletableFuture reloadWithConfig(SessionMcpReloadWithConfigParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.mcp.reloadWithConfig", _p, SessionMcpReloadWithConfigResult.class); + return caller.invoke("session.mcp.reloadWithConfig", _p, McpStartServersResult.class); } /** - * Identifiers and raw MCP CreateMessageRequest params used to run a sampling inference. + * Runs an MCP sampling inference on behalf of an MCP server. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -188,7 +202,7 @@ public CompletableFuture executeSampling(Sessio } /** - * The requestId previously passed to executeSampling that should be cancelled. + * Cancels an in-flight MCP sampling execution by request ID. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -204,7 +218,7 @@ public CompletableFuture cancelSampling } /** - * Mode controlling how MCP server env values are resolved (`direct` or `indirect`). + * Sets how environment-variable values supplied to MCP servers are resolved (direct or indirect). *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -220,7 +234,7 @@ public CompletableFuture setEnvValueMode(Sessio } /** - * Identifies the target session. + * Removes the auto-managed `github` MCP server when present. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -231,7 +245,7 @@ public CompletableFuture removeGitHub() { } /** - * Credential-free authentication identity used to configure GitHub MCP. + * Configures the built-in GitHub MCP server for the session's current auth context. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -240,14 +254,14 @@ public CompletableFuture removeGitHub() { * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture configureGitHub(SessionMcpConfigureGitHubParams params) { + CompletableFuture configureGitHub(SessionMcpConfigureGitHubParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.mcp.configureGitHub", _p, SessionMcpConfigureGitHubResult.class); + return caller.invoke("session.mcp.configureGitHub", _p, McpConfigureGitHubResult.class); } /** - * 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. + * Starts an individual MCP server on the live session. Omit `config` for a config-free start-by-name of an already-configured server (reuses the server's already-registered configuration); supply `config` to start from a caller-supplied configuration. Session-scoped and ephemeral: the server is added to this session's running set only and is reaped when the session ends. Does NOT modify persistent user configuration (`mcp.config.*`), so it does not affect future sessions. The server surfaces through `session.mcp.list` and the `session.mcp_servers_loaded` / `session.mcp_server_status_changed` events like any other server. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -263,7 +277,7 @@ public CompletableFuture startServer(SessionMcpStartServerParams params) { } /** - * 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. + * Starts an individual MCP server on the live session. Omit `config` for a config-free start-by-name of an already-configured server (reuses the server's already-registered configuration); supply `config` to start from a caller-supplied configuration. Session-scoped and ephemeral: the server is added to this session's running set only and is reaped when the session ends. Does NOT modify persistent user configuration (`mcp.config.*`), so it does not affect future sessions. The server surfaces through `session.mcp.list` and the `session.mcp_servers_loaded` / `session.mcp_server_status_changed` events like any other server. *

* Accepts the extensible request, including inputs added after the params record. * @@ -278,7 +292,7 @@ public CompletableFuture startServer(SessionMcpStartServerRequest request) } /** - * 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. + * Restarts an individual MCP server on the live session (stops then starts). Omit `config` for a config-free restart-by-name of an already-configured server; supply `config` to restart with a replacement configuration. Session-scoped and ephemeral: does NOT modify persistent user configuration (`mcp.config.*`). *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -294,7 +308,7 @@ public CompletableFuture restartServer(SessionMcpRestartServerParams param } /** - * 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. + * Restarts an individual MCP server on the live session (stops then starts). Omit `config` for a config-free restart-by-name of an already-configured server; supply `config` to restart with a replacement configuration. Session-scoped and ephemeral: does NOT modify persistent user configuration (`mcp.config.*`). *

* Accepts the extensible request, including inputs added after the params record. * @@ -309,7 +323,7 @@ public CompletableFuture restartServer(SessionMcpRestartServerRequest requ } /** - * Server name for an individual MCP server stop. + * Stops an individual MCP server on the session's host. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -325,7 +339,7 @@ public CompletableFuture stopServer(SessionMcpStopServerParams params) { } /** - * Server name for an individual MCP server stop. + * Stops an individual MCP server on the session's host. *

* Accepts the extensible request, including inputs added after the params record. * @@ -340,7 +354,7 @@ public CompletableFuture stopServer(SessionMcpStopServerRequest request) { } /** - * Registration parameters for an external MCP client. + * Registers a pre-connected external MCP client (e.g. IDE) on the session's host. The caller retains lifecycle ownership of the client and transport. Marked internal because the `client` and `transport` arguments are in-process MCP SDK instances that cannot be serialized across the JSON-RPC boundary; once the CLI moves on top of the SDK, external clients will be expressed as transport configs the runtime can construct itself. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -349,14 +363,14 @@ public CompletableFuture stopServer(SessionMcpStopServerRequest request) { * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture registerExternalClient(SessionMcpRegisterExternalClientParams params) { + CompletableFuture registerExternalClient(SessionMcpRegisterExternalClientParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); return caller.invoke("session.mcp.registerExternalClient", _p, Void.class); } /** - * Server name identifying the external client to remove. + * 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. Disappears alongside `registerExternalClient`: once external clients are described to the runtime as config rather than handed in as instances, lifecycle (including deregistration) is owned entirely by the runtime. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -365,14 +379,14 @@ public CompletableFuture registerExternalClient(SessionMcpRegisterExternal * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture unregisterExternalClient(SessionMcpUnregisterExternalClientParams params) { + CompletableFuture unregisterExternalClient(SessionMcpUnregisterExternalClientParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); return caller.invoke("session.mcp.unregisterExternalClient", _p, Void.class); } /** - * Server name to check running status for. + * Checks whether a named MCP server is currently running on the session's host. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpAppsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpAppsApi.java index 6b932855b2..35cd4c57a3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpAppsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpAppsApi.java @@ -32,7 +32,7 @@ public final class SessionMcpAppsApi { } /** - * MCP server and resource URI to fetch. + * Fetch an MCP resource (typically a `ui://` MCP App bundle, per SEP-1865) from a connected server. Requires the `mcp-apps` session capability. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -48,7 +48,7 @@ public CompletableFuture readResource(SessionM } /** - * MCP server to list app-callable tools for. + * List tools that an MCP App view is allowed to call (SEP-1865 visibility filter). Returns tools whose `_meta.ui.visibility` is unset (default `["model","app"]`) or includes `"app"`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -64,7 +64,7 @@ public CompletableFuture listTools(SessionMcpApps } /** - * MCP server, tool name, and arguments to invoke from an MCP App view. + * Call an MCP tool from an MCP App view (SEP-1865). Enforces the visibility check that prevents an app iframe from invoking model-only tools. Returns the standard MCP `CallToolResult`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -80,7 +80,7 @@ public CompletableFuture callTool(SessionMcpAppsCallToolParams params) } /** - * Host context to advertise to MCP App guests. + * Replace the host context returned to MCP App guests on `ui/initialize`. Hosts use this to advertise theme, locale, or other metadata to the guest UI. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -96,7 +96,7 @@ public CompletableFuture setHostContext(SessionMcpAppsSetHostContextParams } /** - * Identifies the target session. + * Read the current host context advertised to MCP App guests. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -107,7 +107,7 @@ public CompletableFuture getHostContext() { } /** - * MCP server to diagnose MCP Apps wiring for. + * Diagnose MCP Apps wiring for a specific MCP server. Reports the session capability, feature-flag state, advertised extension, and how many tools have `_meta.ui` populated. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpConfigureGitHubParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpConfigureGitHubParams.java index e46a0af8f0..138ec1d55a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpConfigureGitHubParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpConfigureGitHubParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionMcpConfigureGitHubParams( +record SessionMcpConfigureGitHubParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Opaque runtime auth info for GitHub MCP configuration. Marked internal: an in-process runtime shape (configureGitHubMcp is a no-op over the wire). */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java index 45679f83a4..b6c940462e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java @@ -31,7 +31,7 @@ public final class SessionMcpHeadersApi { } /** - * MCP headers refresh request id and the host response. + * Responds to a pending MCP dynamic headers refresh request. Hosts that subscribe to `mcp.headers_refresh_required` use this to provide short-lived per-server headers or to indicate that no dynamic headers are available for this refresh. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksInvokeResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredParams.java similarity index 84% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksInvokeResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredParams.java index a111b7af4e..d4d886b5b0 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksInvokeResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Optional output returned by an SDK callback hook. + * Identifies the target session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -23,7 +23,8 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record HooksInvokeResult( - @JsonProperty("output") Object output +public record SessionMcpListConfiguredParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredResult.java new file mode 100644 index 0000000000..b54b5b238c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpListConfiguredResult.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Effective MCP configuration with optional live observations from matching already materialized servers. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpListConfiguredResult( + /** Effective configured MCP servers. */ + @JsonProperty("servers") List servers +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthApi.java index 835962df39..3910d287eb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthApi.java @@ -32,7 +32,7 @@ public final class SessionMcpOauthApi { } /** - * Pending MCP OAuth request ID and host-provided token or cancellation response. + * 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. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -48,7 +48,7 @@ public CompletableFuture handlePendin } /** - * Identifies the MCP server whose persisted OAuth credentials were updated. + * Notifies the session that MCP OAuth authentication succeeded and updated credentials were persisted, so cached tool definitions can be refreshed. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -64,7 +64,7 @@ public CompletableFuture authenticationStateChanged(SessionMcpOauthAuthent } /** - * Effect-free preparation bound to the existing local session, requester and installation, with frozen options. + * Prepares an inert, expiring owned OAuth login bound to the original session requester and exact installation. Does not activate, connect, read credentials or open a browser. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -80,7 +80,7 @@ public CompletableFuture prepareLogin(Session } /** - * Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback success-page copy, and static OAuth client selection. + * 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. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -96,7 +96,7 @@ public CompletableFuture login(SessionMcpOauthLoginP } /** - * Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback success-page copy, and static OAuth client selection. + * 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. *

* Accepts the extensible request, including inputs added after the params record. * @@ -111,7 +111,23 @@ public CompletableFuture login(SessionMcpOauthLoginR } /** - * Remote MCP server name for a passive OAuth status probe. + * Completes a runtime-managed MCP OAuth login after the authorization server redirects to a host-managed callback URL. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture complete(SessionMcpOauthCompleteParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.oauth.complete", _p, Void.class); + } + + /** + * 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. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -127,7 +143,7 @@ public CompletableFuture probe(SessionMcpOauthProbeParams p } /** - * Remote MCP server name for a passive OAuth status probe. + * 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. *

* Accepts the extensible request, including inputs added after the params record. * @@ -142,7 +158,7 @@ public CompletableFuture probe(SessionMcpOauthProbeRequest } /** - * Targets only the original prepared/applying owned login on this exact session requester. + * Cancels the exact owned OAuth login issued to this original session requester, without clearing shared credentials. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -158,7 +174,7 @@ public CompletableFuture cancelLogin(SessionMc } /** - * Pending MCP OAuth request id to respond to. + * Responds to a pending MCP OAuth authorization request by its request id. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthCompleteParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthCompleteParams.java new file mode 100644 index 0000000000..be283f0b36 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthCompleteParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Host-delivered callback for a runtime-managed MCP OAuth login. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpOauthCompleteParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Opaque identifier returned by session.mcp.oauth.login for the pending external callback. */ + @JsonProperty("authorizationId") String 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. */ + @JsonProperty("callbackUrl") String callbackUrl +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthLoginParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthLoginParams.java index d9234d5587..6f16286adf 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthLoginParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthLoginParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * 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. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthLoginRequest.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthLoginRequest.java index 90864e7dec..82219d8042 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthLoginRequest.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthLoginRequest.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * 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. *

* Required inputs are constructor arguments. Optional inputs have fluent setters. * @@ -58,6 +58,10 @@ public final class SessionMcpOauthLoginRequest { @JsonProperty("grantType") private McpOauthLoginGrantType grantType; + /** 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. */ + @JsonProperty("redirectUri") + private String redirectUri; + /** Required for owned login. Consumes the exact prepareLogin handle once. Set forceReauth and display options during preparation, not consumption. */ @JsonProperty("loginId") private String loginId; @@ -147,6 +151,15 @@ public McpOauthLoginGrantType getGrantType() { return grantType; } + /** + * Returns the {@code redirectUri} property. + * + * @return 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. + */ + public String getRedirectUri() { + return redirectUri; + } + /** * Returns the {@code loginId} property. * @@ -242,6 +255,17 @@ public SessionMcpOauthLoginRequest setGrantType(McpOauthLoginGrantType value) { return this; } + /** + * Sets the {@code redirectUri} property. + * + * @param value 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. + * @return this request + */ + public SessionMcpOauthLoginRequest setRedirectUri(String value) { + this.redirectUri = value; + return this; + } + /** * Sets the {@code loginId} property. * diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthLoginResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthLoginResult.java index aaecf8a64a..5063ec97ca 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthLoginResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpOauthLoginResult.java @@ -28,7 +28,9 @@ public record SessionMcpOauthLoginResult( @JsonProperty("loginId") String loginId, /** Explicit outcome for owned sign-in. Manual callers retain their legacy response shape. */ @JsonProperty("status") McpOwnedOauthLoginStatus status, - /** 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. */ - @JsonProperty("authorizationUrl") String authorizationUrl + /** 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. */ + @JsonProperty("authorizationUrl") String authorizationUrl, + /** 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. */ + @JsonProperty("authorizationId") String authorizationId ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsApi.java new file mode 100644 index 0000000000..ec60f552ca --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsApi.java @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code mcp.prompts} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionMcpPromptsApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionMcpPromptsApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Enumerate one page of prompts a connected MCP server exposes (proxies MCP `prompts/list`). Pass `cursor` to continue from a prior result's `nextCursor`. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture list(SessionMcpPromptsListParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.prompts.list", _p, SessionMcpPromptsListResult.class); + } + + /** + * 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. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture get(SessionMcpPromptsGetParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.prompts.get", _p, SessionMcpPromptsGetResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsGetParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsGetParams.java new file mode 100644 index 0000000000..507b55d7d7 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsGetParams.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * MCP server, prompt name, and optional string-valued arguments. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpPromptsGetParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Name of the MCP server hosting the prompt */ + @JsonProperty("serverName") String serverName, + /** The programmatic name of the prompt */ + @JsonProperty("promptName") String promptName, + /** String-valued arguments to pass to the prompt */ + @JsonProperty("arguments") Map arguments +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsGetResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsGetResult.java new file mode 100644 index 0000000000..26f1ee0186 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsGetResult.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * Prompt messages returned by the MCP server without sending them to the model. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpPromptsGetResult( + /** Description of the prompt */ + @JsonProperty("description") String description, + /** Ordered prompt messages */ + @JsonProperty("messages") List messages, + /** MCP result metadata */ + @JsonProperty("_meta") Map meta, + /** Server-provided non-standard result fields */ + @JsonProperty("additionalProperties") Map additionalProperties +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsListParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsListParams.java new file mode 100644 index 0000000000..e1d21eef1a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsListParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * MCP server whose prompts to enumerate. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpPromptsListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Name of the MCP server whose prompts to enumerate */ + @JsonProperty("serverName") String serverName, + /** Opaque MCP pagination cursor from a prior `nextCursor` value */ + @JsonProperty("cursor") String cursor +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsListResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsListResult.java new file mode 100644 index 0000000000..188cb5f862 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpPromptsListResult.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import java.util.Map; +import javax.annotation.processing.Generated; + +/** + * One page of prompts advertised by the named MCP server. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpPromptsListResult( + /** Prompts advertised by the server */ + @JsonProperty("prompts") List prompts, + /** Opaque cursor for the next page, if the server has more prompts */ + @JsonProperty("nextCursor") String nextCursor, + /** MCP result metadata */ + @JsonProperty("_meta") Map meta, + /** Server-provided non-standard result fields */ + @JsonProperty("additionalProperties") Map additionalProperties +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpRegisterExternalClientParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpRegisterExternalClientParams.java index ba5cdc353e..f2d457cdd5 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpRegisterExternalClientParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpRegisterExternalClientParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionMcpRegisterExternalClientParams( +record SessionMcpRegisterExternalClientParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Logical server name for the external client */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpReloadWithConfigParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpReloadWithConfigParams.java index b93733f0e3..409e75308e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpReloadWithConfigParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpReloadWithConfigParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionMcpReloadWithConfigParams( +record SessionMcpReloadWithConfigParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Opaque runtime MCP reload configuration. Marked internal: an in-process runtime shape (reloadMcpServers throws over the wire). */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesApi.java index c1a30e135d..e81f50eb80 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpResourcesApi.java @@ -31,7 +31,7 @@ public final class SessionMcpResourcesApi { } /** - * MCP server and resource URI to fetch. + * Fetch an MCP resource from a connected server by URI (proxies MCP `resources/read`). *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture read(SessionMcpResources } /** - * MCP server whose resources to enumerate. + * Enumerate one page of resources a connected MCP server exposes (proxies MCP `resources/list`). Pass `cursor` to continue from a prior result's `nextCursor`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture list(SessionMcpResources } /** - * MCP server whose resource templates to enumerate. + * Enumerate one page of resource templates a connected MCP server exposes (proxies MCP `resources/templates/list`). Pass `cursor` to continue from a prior result's `nextCursor`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpUnregisterExternalClientParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpUnregisterExternalClientParams.java index 390e998527..d58cc1d58e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpUnregisterExternalClientParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMcpUnregisterExternalClientParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionMcpUnregisterExternalClientParams( +record SessionMcpUnregisterExternalClientParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Server name of the external client to unregister */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataApi.java index 8d7f3dd753..bf97a0ec39 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionMetadataApi.java @@ -31,7 +31,7 @@ public final class SessionMetadataApi { } /** - * Identifies the target session. + * Returns a snapshot of the session's identifying metadata, mode, agent, and remote info. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture snapshot() { } /** - * Identifies the target session. + * Returns the client-owned string metadata persisted with this local session. The metadata is not included in model context, events, telemetry, snapshots, or remote exports. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -53,7 +53,7 @@ public CompletableFuture getClientMetadata() { } /** - * 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. + * Atomically patches the client-owned string metadata persisted with this local session and returns the committed bag. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -69,7 +69,7 @@ public CompletableFuture updateClientMetadata(SessionMetadataUpd } /** - * Identifies the target session. + * Reports whether the local session is currently processing user/agent messages. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -80,7 +80,7 @@ public CompletableFuture isProcessing() { } /** - * Identifies the target session. + * Returns a snapshot of activity flags for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -91,7 +91,7 @@ public CompletableFuture activity() { } /** - * Model identifier and token limits used to compute the context-info breakdown. + * Returns the token breakdown for the session's current context window for a given model. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -107,7 +107,7 @@ public CompletableFuture contextInfo(SessionMe } /** - * Identifies the target session. + * Returns the experimental per-source attribution breakdown of the session's current context window as a flat list of entries (skills, subagents, MCP servers, built-in tools, plugin rollups, system/tool-definition costs, with nesting via parentId), plus the successful compaction count. The heaviest individual messages are available separately via `metadata.getContextHeaviestMessages`. Returns null until the session has initialized its system prompt and tool metadata. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -118,7 +118,7 @@ public CompletableFuture getContextA } /** - * Parameters for the heaviest-messages query. + * Returns the largest individual messages currently in the session's context window, most-expensive first. Companion to `metadata.getContextAttribution`. Returns an empty list until the session has initialized. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -134,7 +134,7 @@ public CompletableFuture getCon } /** - * Updated working-directory/git context to record on the session. + * Records a working-directory/git context change and emits a `session.context_changed` event. 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): a local session's working directory is authoritative and is moved via `metadata.setWorkingDirectory` (or an SDK `session.resume` that supplies a `workingDirectory`), not by this method. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -150,7 +150,7 @@ public CompletableFuture recordContextChange(SessionMetadataRecordContextC } /** - * 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. + * Updates the session's working directory. For local sessions the target is validated first (an absolute path that exists on disk) and the permission primary directory is re-based; a rejected validation fails the call before any session state changes. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -166,7 +166,7 @@ public CompletableFuture setWorkingDir } /** - * Model identifier to use when re-tokenizing the session's existing messages. + * Re-tokenizes the session's existing messages against a model and returns aggregate token totals. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModeApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModeApi.java index 0f4989a920..8c2c2d66da 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModeApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModeApi.java @@ -31,7 +31,7 @@ public final class SessionModeApi { } /** - * Identifies the target session. + * Gets the current agent interaction mode. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture get() { } /** - * Agent interaction mode to apply to the session. + * Sets the current agent interaction mode. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java index 7afe40eb0b..39c8e4df50 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java @@ -31,7 +31,7 @@ public final class SessionModelApi { } /** - * Identifies the target session. + * Gets the session's authoritative model snapshot, including the committed Auto preference and any newer unclaimed Auto preference waiting for a future user turn. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture getCurrent() { } /** - * Target model identifier and optional reasoning effort, summary, capability overrides, and context tier. + * Switches the session to a model and optional reasoning configuration. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture switchTo(SessionModelSwitch } /** - * An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. + * Requests an Auto preference change without changing the session's selected model. The latest unclaimed request wins; the runtime commits it only after a later prompt using the `auto` model mints a usable model and token pair. A `pending` response confirms that the request was accepted, not that it committed. Observe eventual success through `session.model_change`, failure through the ephemeral `session.auto_tier_switch_failed` event, or current unclaimed state through `session.model.getCurrent`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture switchAutoTier(Sessio } /** - * Managed, repository, and CLI model overrides to overlay onto the session at startup. + * Resolves and applies organization-managed and repository model overlays. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -83,14 +83,14 @@ public CompletableFuture switchAutoTier(Sessio * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture applyStartupOverlay(SessionModelApplyStartupOverlayParams params) { + CompletableFuture applyStartupOverlay(SessionModelApplyStartupOverlayParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.model.applyStartupOverlay", _p, SessionModelApplyStartupOverlayResult.class); + return caller.invoke("session.model.applyStartupOverlay", _p, ModelSwitchToResult.class); } /** - * 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. + * Replaces or clears the host-supplied model allowlist for a running session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -106,7 +106,7 @@ public CompletableFuture setAllowedModels(Se } /** - * Reasoning effort level to apply to the currently selected model. + * Updates the session's reasoning effort without changing the selected model. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -122,7 +122,7 @@ public CompletableFuture setReasoningEffor } /** - * Optional listing options. + * Lists models available to this session using its own auth and integration context. Connected hosts (CLI TUI, GitHub App) should call this through the session client so remote sessions return the remote CLI's available models rather than the caller's. *

* Invokes the method with no params, applying the runtime defaults. * @@ -135,7 +135,7 @@ public CompletableFuture list() { } /** - * Optional listing options. + * Lists models available to this session using its own auth and integration context. Connected hosts (CLI TUI, GitHub App) should call this through the session client so remote sessions return the remote CLI's available models rather than the caller's. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayParams.java index 73ae210e78..ad25a718a2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionModelApplyStartupOverlayParams( +record SessionModelApplyStartupOverlayParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Model required by device-managed policy, when configured. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java index 23a9788540..6d6e79a26a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java @@ -26,6 +26,8 @@ public record SessionModelGetCurrentResult( /** Currently active model identifier */ @JsonProperty("modelId") String modelId, + /** 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. */ + @JsonProperty("planBaseModelId") String planBaseModelId, /** 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. */ @JsonProperty("reasoningEffort") String reasoningEffort, /** Context tier for models that support multiple context-window sizes. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionNameApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionNameApi.java index 9bfc5bea59..aeefa0df31 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionNameApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionNameApi.java @@ -31,7 +31,7 @@ public final class SessionNameApi { } /** - * Identifies the target session. + * Gets the session's friendly name. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture get() { } /** - * New friendly name to apply to the session. + * Sets the session's friendly name. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture set(SessionNameSetParams params) { } /** - * Auto-generated session summary to apply as the session's name when no user-set name exists. + * Persists an auto-generated session summary as the session's name when no user-set name exists. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsApi.java index 32d4b76908..06986da2ff 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsApi.java @@ -31,7 +31,7 @@ public final class SessionOptionsApi { } /** - * Patch of mutable session options to apply to the running session. + * Patches the genuinely-mutable subset of session options. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsApi.java index aed2de53fb..eb4cc591e8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsApi.java @@ -44,7 +44,7 @@ public final class SessionPermissionsApi { } /** - * Patch of permission policy fields to apply (omit a field to leave it unchanged). + * Replaces selected permission policy fields (rules, paths, URLs, exclusions, allow-all flags) on the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -60,7 +60,7 @@ public CompletableFuture configure(SessionPer } /** - * Pending permission request ID and the decision to apply (approve/reject and scope). + * Provides a decision for a pending tool permission request. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture } /** - * No parameters; returns currently-pending permission requests for the session. + * Reconstructs the set of pending tool permission requests from the session's event history. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -87,7 +87,7 @@ public CompletableFuture pendingRequest } /** - * Allow-all toggle for tool permission requests, with an optional telemetry source. + * Enables or disables automatic approval of tool permission requests for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -103,7 +103,7 @@ public CompletableFuture setApproveAll(Se } /** - * Permission mode to apply for the session. + * Sets the permission mode for the session. `manual` follows the normal approval flow, `assisted` attaches LLM safety recommendations, and `allow-all` automatically approves permission requests. The result returns the authoritative post-mutation mode so callers can update local state without racing the `session.permissions_changed` notification. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -119,7 +119,7 @@ public CompletableFuture setMode(SessionPermiss } /** - * No parameters. + * Returns the current permission mode for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -130,7 +130,7 @@ public CompletableFuture getMode() { } /** - * Scope and add/remove instructions for modifying session- or location-scoped permission rules. + * Adds or removes session-scoped or location-scoped permission rules. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -146,7 +146,7 @@ public CompletableFuture modifyRules(Sessio } /** - * Toggles whether permission prompts should be bridged into session events for this client. + * Sets whether the client wants permission prompts bridged into session events. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -162,7 +162,7 @@ public CompletableFuture setRequired(Sessio } /** - * Clears session-scoped tool approvals and optionally clears location-scoped approvals and exact session-approved paths. + * Clears session-scoped tool approvals and, for full resets, exact session-approved paths. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -178,7 +178,7 @@ public CompletableFuture resetSes } /** - * Notification payload describing the permission prompt that the client just rendered. + * Notifies the runtime that a permission prompt UI has been shown to the user. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsFolderTrustApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsFolderTrustApi.java index 55bac08941..741e4ed737 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsFolderTrustApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsFolderTrustApi.java @@ -31,7 +31,7 @@ public final class SessionPermissionsFolderTrustApi { } /** - * Folder path to check for trust. + * Reports whether a folder is trusted according to the user's folder trust state. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture isTrusted } /** - * Folder path to add to trusted folders. + * Adds a folder to the user's trusted folders list. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsLocationsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsLocationsApi.java index 46ce8ef4d3..37f16bf40c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsLocationsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsLocationsApi.java @@ -31,7 +31,7 @@ public final class SessionPermissionsLocationsApi { } /** - * Working directory to resolve into a location-permissions key. + * Resolves the permission location key and type for a working directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture resolve(Sessi } /** - * Working directory to load persisted location permissions for. + * Applies persisted location-scoped tool approvals and allowed directories for a working directory to this session's permission service. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture apply(SessionPe } /** - * Location-scoped tool approval to persist. + * Persists a tool approval for a permission location and applies its rules to this session's live permission service. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsPathsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsPathsApi.java index ecf9d81b52..62ae0a193b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsPathsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsPathsApi.java @@ -31,7 +31,7 @@ public final class SessionPermissionsPathsApi { } /** - * No parameters; returns the session's recursive directory grants and exact session-approved paths. + * Returns the session's recursive directory grants, exact session-approved paths, and primary working directory. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture list() { } /** - * Directory path to add to the session's allowed directories. + * Adds a directory to the session's allow-list and activates conventional skill and agent definitions under it. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture add(SessionPermission } /** - * Directory path to set as the session's new primary working directory. + * Updates the session's primary working directory used by the permission policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture updatePrima } /** - * Path to evaluate against the session's allowed directories. + * Reports whether a path falls within any of the session's allowed directories. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -90,7 +90,7 @@ public CompletableFuture * Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsUrlsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsUrlsApi.java index 5ca15960d2..3261b07033 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsUrlsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPermissionsUrlsApi.java @@ -31,7 +31,7 @@ public final class SessionPermissionsUrlsApi { } /** - * Whether the URL-permission policy should run in unrestricted mode. + * Toggles the runtime's URL-permission policy between unrestricted and restricted modes. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPlanApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPlanApi.java index 7805183ddf..daf85af7db 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPlanApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPlanApi.java @@ -31,7 +31,7 @@ public final class SessionPlanApi { } /** - * Identifies the target session. + * Reads the session plan file from the workspace. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture read() { } /** - * Replacement contents to write to the session plan file. + * Writes new content to the session plan file. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture update(SessionPlanUpdateParams params) { } /** - * Identifies the target session. + * Deletes the session plan file from the workspace. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -69,7 +69,7 @@ public CompletableFuture delete() { } /** - * Identifies the target session. + * Reads todo rows from the session SQL database for plan rendering. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -80,7 +80,7 @@ public CompletableFuture readSqlTodos() { } /** - * Identifies the target session. + * Reads todo rows AND dependency edges from the session SQL database for structured progress UI. Same defensive behavior as readSqlTodos — returns empty arrays when the database, tables, or columns aren't available. Clients should call this on session start and after every `session.todos_changed` event to refresh structured-UI rendering. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsApi.java index 6a49785b30..e041a149c1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsApi.java @@ -35,7 +35,7 @@ public final class SessionPluginsApi { } /** - * Identifies the target session. + * Lists globally installed, live, built-in, and enterprise-managed desired plugins using the live session's authoritative account, working directory, and retained managed policy. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -46,7 +46,7 @@ public CompletableFuture list() { } /** - * Plugin source resolved relative to the session's authoritative working directory. + * Installs a plugin using the live session's authoritative account, working directory, and retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -62,7 +62,7 @@ public CompletableFuture install(SessionPluginsInst } /** - * Name (or spec) of the plugin to uninstall. + * Uninstalls a plugin when permitted by the live session's retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -78,7 +78,7 @@ public CompletableFuture uninstall(SessionPluginsUninstallParams params) { } /** - * Name (or spec) of the plugin to update. + * Updates an installed plugin using the live session's authoritative account, working directory, and retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -94,7 +94,7 @@ public CompletableFuture update(SessionPluginsUpdate } /** - * Plugin names (or specs) to enable in the session's authoritative working directory. + * Enables installed plugins when permitted by the live session's retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -110,7 +110,7 @@ public CompletableFuture enable(SessionPluginsEnableParams params) { } /** - * Plugin names (or specs) to disable in the session's authoritative working directory. + * Disables installed plugins when permitted by the live session's retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -126,7 +126,7 @@ public CompletableFuture disable(SessionPluginsDisableParams params) { } /** - * Optional flags controlling which side effects the reload performs. + * Reloads the session's plugin set, refreshing MCP servers, custom agents, hooks, and skills cache so SDK-driven changes via `server.plugins.*` take effect immediately. *

* Invokes the method with no params, applying the runtime defaults. * @@ -139,7 +139,7 @@ public CompletableFuture reload() { } /** - * Optional flags controlling which side effects the reload performs. + * Reloads the session's plugin set, refreshing MCP servers, custom agents, hooks, and skills cache so SDK-driven changes via `server.plugins.*` take effect immediately. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsMarketplacesApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsMarketplacesApi.java index 3ec14c5cf7..c9682c21f8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsMarketplacesApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionPluginsMarketplacesApi.java @@ -31,7 +31,7 @@ public final class SessionPluginsMarketplacesApi { } /** - * Identifies the target session. + * Lists registered and enterprise-managed desired marketplaces using the live session's retained policy. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture list() { } /** - * Marketplace source and optional working directory for relative-path resolution. + * Adds a marketplace when permitted by the live session's retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture add(SessionPlugins } /** - * Name of the marketplace to remove and an optional force flag. + * Removes a marketplace when permitted by the live session's retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture remove(SessionP } /** - * Name of the marketplace whose plugin catalog to fetch. + * Browses a marketplace resolved through the live session's working directory and retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -90,7 +90,7 @@ public CompletableFuture browse(SessionP } /** - * Optional marketplace name; omit to refresh all. + * Refreshes marketplaces resolved through the live session's working directory and retained managed policy. *

* Invokes the method with no params, applying the runtime defaults. * @@ -103,7 +103,7 @@ public CompletableFuture refresh() { } /** - * Optional marketplace name; omit to refresh all. + * Refreshes marketplaces resolved through the live session's working directory and retained managed policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderApi.java index bdf0215f90..8d3b283405 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderApi.java @@ -31,7 +31,7 @@ public final class SessionProviderApi { } /** - * Optional model identifier to scope the endpoint snapshot to. + * Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses. *

* Invokes the method with no params, applying the runtime defaults. * @@ -44,7 +44,7 @@ public CompletableFuture getEndpoint() { } /** - * Optional model identifier to scope the endpoint snapshot to. + * Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -60,7 +60,7 @@ public CompletableFuture getEndpoint(SessionPr } /** - * BYOK providers and/or models to add to the session's registry at runtime. Both fields are optional; provide providers, models, or both. + * Adds BYOK providers and/or models to the session's registry at runtime, extending the additive registry built from the session's `providers`/`models` options. Both fields are optional, so a call may add providers only, models only, or both. Within a single call providers are registered before models, so a model may reference a provider added in the same call; across calls a model may reference any provider already registered (from session creation or a prior add). A model whose referenced provider is not registered by the end of the call is rejected. Newly added models become selectable via `model.list` / `model.switchTo` and are inherited by sub-agents spawned afterwards. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,7 +76,7 @@ public CompletableFuture add(SessionProviderAddParams } /** - * Authoritative BYOK provider and model registry snapshot to apply atomically to the session. + * Atomically updates the session's BYOK provider and model registry by applying the supplied snapshot, replacing existing entries, updating models, or removing entries absent from the snapshot. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -91,4 +91,20 @@ public CompletableFuture sync(SessionProviderSyncPara return caller.invoke("session.provider.sync", _p, SessionProviderSyncResult.class); } + /** + * Withdraws named host-managed models from the session's BYOK registry, leaving every other entry untouched. The scoped counterpart to `provider.sync`: a snapshot can only describe entries the caller knows about, so using it to remove one model silently withdraws rows registered by another source, such as a plugin calling `provider.add` at runtime. Naming what to remove leaves unrelated entries alone. Selection ids that are not registered are ignored, so withdrawal is idempotent. A provider is removed only when one of the withdrawn models was the last entry referencing it; a provider that simply has no models, which is the normal state while its rows are supplied by catalog discovery, is left in place. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture withdraw(SessionProviderWithdrawParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.provider.withdraw", _p, SessionProviderWithdrawResult.class); + } + } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderWithdrawParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderWithdrawParams.java new file mode 100644 index 0000000000..8fbc84286d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderWithdrawParams.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Host-managed model selection ids to withdraw from the session's BYOK registry. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProviderWithdrawParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** 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. */ + @JsonProperty("models") List models +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderWithdrawResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderWithdrawResult.java new file mode 100644 index 0000000000..4915e82aaf --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProviderWithdrawResult.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * What the withdrawal actually removed from the registry. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProviderWithdrawResult( + /** Selection ids that were registered and are now withdrawn. Excludes requested ids that were not present. */ + @JsonProperty("withdrawn") List withdrawn, + /** Providers removed because one of the withdrawn models was the last entry referencing them. A provider that merely has no models is not removed. */ + @JsonProperty("providersRemoved") List providersRemoved, + /** 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. */ + @JsonProperty("modelDeselected") Boolean modelDeselected +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersApi.java new file mode 100644 index 0000000000..f7dde45999 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersApi.java @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code providers} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionProvidersApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** API methods for the {@code providers.models} sub-namespace. */ + public final SessionProvidersModelsApi models; + + /** @param caller the RPC transport function */ + SessionProvidersApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + this.models = new SessionProvidersModelsApi(caller, sessionId); + } + + /** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture getCatalog() { + return caller.invoke("session.providers.getCatalog", java.util.Map.of("sessionId", this.sessionId), SessionProvidersGetCatalogResult.class); + } + + /** + * Discovers reachable instances using an adapter from this session's effective provider catalog and provider-specific discovery input. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture discover(SessionProvidersDiscoverParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.providers.discover", _p, SessionProvidersDiscoverResult.class); + } + + /** + * Gets current health and version information for a discovered model-provider instance. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture getStatus(SessionProvidersGetStatusParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.providers.getStatus", _p, SessionProvidersGetStatusResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersDiscoverParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersDiscoverParams.java new file mode 100644 index 0000000000..f5400285f8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersDiscoverParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Provider discovery parameters. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersDiscoverParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Opaque adapter identity returned by `session.providers.getCatalog`. */ + @JsonProperty("adapterId") String 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. */ + @JsonProperty("input") Object input +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsReloadResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersDiscoverResult.java similarity index 67% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsReloadResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersDiscoverResult.java index 5f6fa34306..1467adc6a9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionCustomizationsReloadResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersDiscoverResult.java @@ -15,7 +15,7 @@ import javax.annotation.processing.Generated; /** - * Diagnostics from reloading skill definitions, with warnings and errors as separate lists. + * Provider instances found by a discovery operation. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -24,10 +24,10 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionCustomizationsReloadResult( - /** Warnings emitted while loading skills (e.g. skills that loaded but had issues) */ - @JsonProperty("warnings") List warnings, - /** Errors emitted while loading skills (e.g. skills that failed to load entirely) */ - @JsonProperty("errors") List errors +public record SessionProvidersDiscoverResult( + /** Typed operation outcome. Passive discovery can return `absent` with an empty instance list. */ + @JsonProperty("outcome") ModelProviderOperationOutcome outcome, + /** Discovered provider instances. Empty when passive default discovery finds no reachable provider. */ + @JsonProperty("instances") List instances ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetCatalogParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetCatalogParams.java new file mode 100644 index 0000000000..1fd343bb0a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetCatalogParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersGetCatalogParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetCatalogResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetCatalogResult.java new file mode 100644 index 0000000000..49c23ba195 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetCatalogResult.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Normalized model-provider adapter definitions available to the session, not discovered instances. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersGetCatalogResult( + /** Available provider adapters ordered by adapterId. */ + @JsonProperty("providers") List providers +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetStatusParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetStatusParams.java new file mode 100644 index 0000000000..56e686dd30 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetStatusParams.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Provider status request parameters. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersGetStatusParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Provider instance reference returned by discovery. */ + @JsonProperty("instance") ModelProviderInstanceReference instance +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetStatusResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetStatusResult.java new file mode 100644 index 0000000000..cff3c86ed8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersGetStatusResult.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Current health information for a provider instance. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersGetStatusResult( + /** Typed operation outcome. */ + @JsonProperty("outcome") ModelProviderOperationOutcome outcome, + /** Normalized provider instance. */ + @JsonProperty("instance") ModelProviderInstance instance, + /** Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`. */ + @JsonProperty("status") String status, + /** Provider-reported version. */ + @JsonProperty("version") String version +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsApi.java new file mode 100644 index 0000000000..3f5855d9a8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsApi.java @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code providers.models} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionProvidersModelsApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionProvidersModelsApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Lists models installed or otherwise available from a discovered model-provider instance. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture list(SessionProvidersModelsListParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.providers.models.list", _p, SessionProvidersModelsListResult.class); + } + + /** + * 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. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture prepareConfiguration(SessionProvidersModelsPrepareConfigurationParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.providers.models.prepareConfiguration", _p, SessionProvidersModelsPrepareConfigurationResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsListParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsListParams.java new file mode 100644 index 0000000000..89f4984ae4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsListParams.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Provider model inventory request parameters. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersModelsListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Provider instance reference returned by discovery. */ + @JsonProperty("instance") ModelProviderInstanceReference instance +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsListResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsListResult.java new file mode 100644 index 0000000000..307ee6728b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsListResult.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersModelsListResult( + /** Typed operation outcome. */ + @JsonProperty("outcome") ModelProviderOperationOutcome outcome, + /** Provider-native models in provider order. */ + @JsonProperty("models") List models +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsPrepareConfigurationParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsPrepareConfigurationParams.java new file mode 100644 index 0000000000..60548516cb --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsPrepareConfigurationParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * 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`. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersModelsPrepareConfigurationParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The discovered instance that serves the model. */ + @JsonProperty("instance") ModelProviderInstance instance, + /** The discovered model to configure. */ + @JsonProperty("model") DiscoveredModel model +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsPrepareConfigurationResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsPrepareConfigurationResult.java new file mode 100644 index 0000000000..e2f2abeebb --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionProvidersModelsPrepareConfigurationResult.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * 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. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionProvidersModelsPrepareConfigurationResult( + /** Provider connection prepared from the instance's inference metadata. Carries no credential; supply one if the endpoint requires it. */ + @JsonProperty("provider") NamedProviderConfig provider, + /** Model definition prepared from the discovered model. Capability fields the provider did not report stay omitted rather than being asserted false. */ + @JsonProperty("model") ProviderModelConfig model, + /** Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is applied. */ + @JsonProperty("selectionId") String selectionId, + /** 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. */ + @JsonProperty("providerDisposition") ModelProviderConfigurationDisposition providerDisposition, + /** Whether `model` still needs to be registered. When `alreadyConfigured`, `selectionId` is already registered and the caller can select it without adding anything. */ + @JsonProperty("modelDisposition") ModelProviderConfigurationDisposition modelDisposition, + /** Non-fatal warnings carried over from the discovered model, such as capabilities the provider did not report. */ + @JsonProperty("warnings") List warnings +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueApi.java index 5e6d67690c..e38110708f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueApi.java @@ -31,7 +31,7 @@ public final class SessionQueueApi { } /** - * Identifies the target session. + * Returns the local session's pending user-facing queued items and steering messages. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,18 +42,18 @@ public CompletableFuture pendingItems() { } /** - * Identifies the target session. + * Returns the internal native queue snapshot for in-process session orchestration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture snapshot() { - return caller.invoke("session.queue.snapshot", java.util.Map.of("sessionId", this.sessionId), SessionQueueSnapshotResult.class); + CompletableFuture snapshot() { + return caller.invoke("session.queue.snapshot", java.util.Map.of("sessionId", this.sessionId), QueueSnapshotResult.class); } /** - * Parameters for moving a queued item by stable id. + * Moves an addressable queued item to a public visible position. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -69,7 +69,7 @@ public CompletableFuture moveItem(SessionQueueMoveIt } /** - * Parameters for inserting a queued message at a public visible position. + * Inserts a new queued message at a public visible position. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -85,7 +85,7 @@ public CompletableFuture insertAt(SessionQueueInsert } /** - * Parameters for removing a queued item by stable id. + * Removes an addressable queued item by its stable id. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -101,7 +101,7 @@ public CompletableFuture removeAt(SessionQueueRemove } /** - * Parameters for editing a single queued message. + * Updates the text of an addressable single-message queue item. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -117,7 +117,7 @@ public CompletableFuture updateText(SessionQueueUp } /** - * Conditional withdrawal of a single user message, from its queue or from the running turn it started. + * Atomically withdraws an unchanged user message of a local session: from the queued or steering lane while unconsumed, or from the running turn it started while the model has not answered it and nothing the user sent after it is pending. Withdrawing from the running turn interrupts that turn and removes its events from history. A client retaining the original draft may restore it only when removed is true. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -133,7 +133,7 @@ public CompletableFuture withdrawMessage(Sess } /** - * Append to one pending steering message without changing its identity or delivery position. + * Atomically appends text and attachments to an unchanged, unconsumed local steering message. Returns updated=false if delivery or withdrawal already claimed the message. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -149,7 +149,7 @@ public CompletableFuture appendSteering(Sessio } /** - * Parameters for duplicating a queued item. + * Duplicates an addressable queued item immediately after its source. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -165,7 +165,7 @@ public CompletableFuture duplicateAt(SessionQueue } /** - * 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. + * Acquires or releases the queued-lane drain pause. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -181,7 +181,7 @@ public CompletableFuture setDrainPaused(SessionQueueSetDrainPausedParams p } /** - * Parameters for steering a queued message into a live turn. + * Moves an addressable queued message into the live turn's steering lane. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -197,18 +197,18 @@ public CompletableFuture sendNow(SessionQueueSendNowP } /** - * Identifies the target session. + * Reports whether the local session has native queued work pending. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture hasPending() { - return caller.invoke("session.queue.hasPending", java.util.Map.of("sessionId", this.sessionId), SessionQueueHasPendingResult.class); + CompletableFuture hasPending() { + return caller.invoke("session.queue.hasPending", java.util.Map.of("sessionId", this.sessionId), QueueHasPendingResult.class); } /** - * Inputs for starting a deferred-idle drain. + * Begins a native deferred-idle drain when background work has quiesced. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -217,14 +217,14 @@ public CompletableFuture hasPending() { * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture beginDeferredIdleDrain(SessionQueueBeginDeferredIdleDrainParams params) { + CompletableFuture beginDeferredIdleDrain(SessionQueueBeginDeferredIdleDrainParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.queue.beginDeferredIdleDrain", _p, SessionQueueBeginDeferredIdleDrainResult.class); + return caller.invoke("session.queue.beginDeferredIdleDrain", _p, QueueBeginDeferredIdleDrainResult.class); } /** - * Inputs for completing a deferred-idle drain. + * Finishes a native deferred-idle drain and reports whether to drain queue work or emit idle. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -233,14 +233,14 @@ public CompletableFuture beginDeferred * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture finishDeferredIdleDrain(SessionQueueFinishDeferredIdleDrainParams params) { + CompletableFuture finishDeferredIdleDrain(SessionQueueFinishDeferredIdleDrainParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.queue.finishDeferredIdleDrain", _p, SessionQueueFinishDeferredIdleDrainResult.class); + return caller.invoke("session.queue.finishDeferredIdleDrain", _p, QueueFinishDeferredIdleDrainResult.class); } /** - * Inputs for marking session.idle deferred in native state. + * Marks session.idle as deferred by native background work state. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -249,14 +249,14 @@ public CompletableFuture finishDeferr * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture deferSessionIdle(SessionQueueDeferSessionIdleParams params) { + CompletableFuture deferSessionIdle(SessionQueueDeferSessionIdleParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); return caller.invoke("session.queue.deferSessionIdle", _p, Void.class); } /** - * Identifies the target session. + * Removes the most recently queued user-facing item (LIFO). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -267,7 +267,7 @@ public CompletableFuture removeMostRecent() } /** - * Identifies the target session. + * Clears all pending queued items on the local session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -278,7 +278,7 @@ public CompletableFuture clear() { } /** - * Internal filter for consuming queued system notifications. + * Consumes queued native system notifications matching an internal filter. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -287,31 +287,31 @@ public CompletableFuture clear() { * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture consumeSystemNotifications(SessionQueueConsumeSystemNotificationsParams params) { + CompletableFuture consumeSystemNotifications(SessionQueueConsumeSystemNotificationsParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.queue.consumeSystemNotifications", _p, SessionQueueConsumeSystemNotificationsResult.class); + return caller.invoke("session.queue.consumeSystemNotifications", _p, QueueRemoveMostRecentResult.class); } /** - * Identifies the target session. + * Enqueues the internal resume-pending wake item when orphan handling needs a follow-up turn. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture enqueueResumePending() { - return caller.invoke("session.queue.enqueueResumePending", java.util.Map.of("sessionId", this.sessionId), SessionQueueEnqueueResumePendingResult.class); + CompletableFuture enqueueResumePending() { + return caller.invoke("session.queue.enqueueResumePending", java.util.Map.of("sessionId", this.sessionId), QueueEnqueueResumePendingResult.class); } /** - * Identifies the target session. + * Drains the native local-session work queue for in-process session orchestration. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture process() { + CompletableFuture process() { return caller.invoke("session.queue.process", java.util.Map.of("sessionId", this.sessionId), Void.class); } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueBeginDeferredIdleDrainParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueBeginDeferredIdleDrainParams.java index 4973e31075..a8e3790e7e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueBeginDeferredIdleDrainParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueBeginDeferredIdleDrainParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionQueueBeginDeferredIdleDrainParams( +record SessionQueueBeginDeferredIdleDrainParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Whether the host still has active background work. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueConsumeSystemNotificationsParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueConsumeSystemNotificationsParams.java index 6449ce7666..3b1d844881 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueConsumeSystemNotificationsParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueConsumeSystemNotificationsParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionQueueConsumeSystemNotificationsParams( +record SessionQueueConsumeSystemNotificationsParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Opaque runtime-owned filter object. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueDeferSessionIdleParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueDeferSessionIdleParams.java index 7b3dff9ef5..ec96cf762e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueDeferSessionIdleParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueDeferSessionIdleParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionQueueDeferSessionIdleParams( +record SessionQueueDeferSessionIdleParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Whether the deferred idle was caused by an aborted foreground turn. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueEnqueueResumePendingParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueEnqueueResumePendingParams.java index 1a0ec546ae..6817219452 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueEnqueueResumePendingParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueEnqueueResumePendingParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionQueueEnqueueResumePendingParams( +record SessionQueueEnqueueResumePendingParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueFinishDeferredIdleDrainParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueFinishDeferredIdleDrainParams.java index b6b29057d8..43568d4cd9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueFinishDeferredIdleDrainParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueFinishDeferredIdleDrainParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionQueueFinishDeferredIdleDrainParams( +record SessionQueueFinishDeferredIdleDrainParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Whether the host still has active background work. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueHasPendingParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueHasPendingParams.java index e587fec4cb..4b8ef92b5f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueHasPendingParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueHasPendingParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionQueueHasPendingParams( +record SessionQueueHasPendingParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueProcessParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueProcessParams.java index 8c8edfe554..76c805ac4c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueProcessParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueProcessParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionQueueProcessParams( +record SessionQueueProcessParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueSnapshotParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueSnapshotParams.java index dff5db8a8d..7963b017c4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueSnapshotParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionQueueSnapshotParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionQueueSnapshotParams( +record SessionQueueSnapshotParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRemoteApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRemoteApi.java index bb1cfa0e25..015c69f830 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRemoteApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRemoteApi.java @@ -31,7 +31,7 @@ public final class SessionRemoteApi { } /** - * Optional remote session mode ("off", "export", or "on"); defaults to enabling both export and remote steering. + * Enables remote session export or steering. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture enable(SessionRemoteEnablePa } /** - * Identifies the target session. + * Disables remote session export and steering. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -58,7 +58,7 @@ public CompletableFuture disable() { } /** - * New remote-steerability state to persist as a `session.remote_steerable_changed` event. + * Persists a remote-steerability change emitted by the host as a session event. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java index bb3bb0680b..5ff78a1dc3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java @@ -29,6 +29,8 @@ public final class SessionRpc { private final RpcCaller caller; private final String sessionId; + /** API methods for the {@code providers} namespace. */ + public final SessionProvidersApi providers; /** API methods for the {@code sandbox} namespace. */ public final SessionSandboxApi sandbox; /** API methods for the {@code gitHubAuth} namespace. */ @@ -98,7 +100,7 @@ public final class SessionRpc { /** API methods for the {@code metadata} namespace. */ public final SessionMetadataApi metadata; /** API methods for the {@code settings} namespace. */ - public final SessionSettingsApi settings; + final SessionSettingsApi settings; /** API methods for the {@code contentExclusion} namespace. */ public final SessionContentExclusionApi contentExclusion; /** API methods for the {@code shell} namespace. */ @@ -129,6 +131,7 @@ public final class SessionRpc { public SessionRpc(RpcCaller caller, String sessionId) { this.caller = caller; this.sessionId = sessionId; + this.providers = new SessionProvidersApi(caller, sessionId); this.sandbox = new SessionSandboxApi(caller, sessionId); this.gitHubAuth = new SessionGitHubAuthApi(caller, sessionId); this.accounts = new SessionAccountsApi(caller, sessionId); @@ -177,7 +180,7 @@ public SessionRpc(RpcCaller caller, String sessionId) { } /** - * Identifies the target session. + * Suspends the session while preserving persisted state for later resume. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -188,7 +191,7 @@ public CompletableFuture suspend() { } /** - * Parameters for sending a user message to the session + * Sends a user message to the session and returns its message ID. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -204,7 +207,7 @@ public CompletableFuture send(SessionSendParams params) { } /** - * Parameters for sending zero or more user messages to the session in a single turn. Remote-backed (Mission Control) sessions do not support this method and will return an error. + * Sends zero or more user messages to the session in a single turn and returns their message IDs. All provided messages are appended to the conversation in order, then exactly one agent turn runs over the resulting history. When the list is empty, one turn runs over the existing history with no new user message. Remote-backed (Mission Control) sessions do not support this method and will return an error. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -220,7 +223,7 @@ public CompletableFuture sendMessages(SessionSendMess } /** - * Internal request for sending a system notification. + * Queues or sends an internal system notification to the session according to its passive policy. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -229,14 +232,14 @@ public CompletableFuture sendMessages(SessionSendMess * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture sendSystemNotification(SessionSendSystemNotificationParams params) { + CompletableFuture sendSystemNotification(SessionSendSystemNotificationParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); return caller.invoke("session.sendSystemNotification", _p, Void.class); } /** - * Parameters for aborting the current turn + * Aborts the current agent turn. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -252,7 +255,7 @@ public CompletableFuture abort(SessionAbortParams params) { } /** - * Parameters for interrupting the main agent turn. + * Interrupts the current main agent turn while leaving running background work (subagents, sidekicks, and promoted attached shells) alive. No-op when the main loop is not processing. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -268,7 +271,7 @@ public CompletableFuture interruptMainTurn(Sessi } /** - * Identifies the target session. + * Cancels every running background agent (task-registry subagents plus sidekick agents) without interrupting the main agent loop. Promoted attached shells are left running. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -279,7 +282,7 @@ public CompletableFuture cancelAllBackgroundAgents() { } /** - * Parameters for shutting down the session + * Shuts down the session and persists its final state. Awaits any deferred sessionEnd hooks before resolving so user-supplied hook scripts complete before the runtime tears down. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -295,7 +298,7 @@ public CompletableFuture shutdown(SessionShutdownParams params) { } /** - * Message text, optional severity level, persistence flag, optional follow-up URL, and optional tip. + * Emits a user-visible session log event. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxApi.java index a13ef559ef..2ace81bb8e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSandboxApi.java @@ -31,7 +31,7 @@ public final class SessionSandboxApi { } /** - * Identifies the target session. + * Returns whether managed policy requires sandbox enforcement and whether an enforcement failure has permanently blocked the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture getEnforcemen } /** - * Request to disable sandboxing for the current session while resolving an active sandbox-bypass permission prompt. + * Disables sandboxing for the remainder of the current session and approves the referenced pending sandbox-bypass permission request. The request is rejected unless the exact request is still pending and the effective sandbox policy permits bypass. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture disableForSessio } /** - * Request to accept the sandbox path grant offered on an active sandbox escalation permission prompt. + * Adds the path offered by a pending sandbox escalation permission request's sandboxPathGrant to the session's sandbox policy and approves the request, so the blocked operation re-runs inside the sandbox rather than outside it. The request is rejected unless the exact request is still pending, carries a sandboxPathGrant, and the grant still takes effect under the current managed policy. Does not persist the path; hosts that store sandbox settings save it themselves. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddAtParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddAtParams.java index 0a099bdf4a..11a8ac65d2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddAtParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddAtParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionScheduleAddAtParams( +record SessionScheduleAddAtParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Epoch milliseconds when the prompt should fire. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddCronParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddCronParams.java index 08a9cd33bb..732fe8b343 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddCronParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddCronParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionScheduleAddCronParams( +record SessionScheduleAddCronParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** 5-field cron expression. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddCronResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddCronResult.java deleted file mode 100644 index 193dea1b4c..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddCronResult.java +++ /dev/null @@ -1,32 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; -import javax.annotation.processing.Generated; - -/** - * Result of registering or re-arming a scheduled prompt. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ -@CopilotExperimental -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record SessionScheduleAddCronResult( - /** The registered or updated schedule entry. */ - @JsonProperty("entry") ScheduleEntry entry, - /** User-facing validation error, when registration failed. */ - @JsonProperty("error") String error -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddParams.java index 31580758cb..b388ce49db 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionScheduleAddParams( +record SessionScheduleAddParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Human-readable interval such as `30s`, `5m`, or `2h`. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddSelfPacedParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddSelfPacedParams.java index 17a89c1b2d..f2fa5e2860 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddSelfPacedParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleAddSelfPacedParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionScheduleAddSelfPacedParams( +record SessionScheduleAddSelfPacedParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Prompt text to enqueue when the schedule fires. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleApi.java index f983f84f71..0b6a90aca9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleApi.java @@ -31,7 +31,7 @@ public final class SessionScheduleApi { } /** - * Identifies the target session. + * Lists the session's currently active scheduled prompts. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,29 +42,29 @@ public CompletableFuture list() { } /** - * Identifies the target session. + * Hydrates the native schedule registry from persisted session events. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture hydrate() { + CompletableFuture hydrate() { return caller.invoke("session.schedule.hydrate", java.util.Map.of("sessionId", this.sessionId), Void.class); } /** - * Identifies the target session. + * Reports whether the session has an active self-paced scheduled prompt. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture hasSelfPaced() { - return caller.invoke("session.schedule.hasSelfPaced", java.util.Map.of("sessionId", this.sessionId), SessionScheduleHasSelfPacedResult.class); + CompletableFuture hasSelfPaced() { + return caller.invoke("session.schedule.hasSelfPaced", java.util.Map.of("sessionId", this.sessionId), ScheduleHasSelfPacedResult.class); } /** - * Register a relative-interval scheduled prompt. + * Registers a relative-interval scheduled prompt. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -73,14 +73,14 @@ public CompletableFuture hasSelfPaced() { * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture add(SessionScheduleAddParams params) { + CompletableFuture add(SessionScheduleAddParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.schedule.add", _p, SessionScheduleAddResult.class); + return caller.invoke("session.schedule.add", _p, ScheduleAddResult.class); } /** - * Register a cron scheduled prompt. + * Registers a recurring cron scheduled prompt. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -89,14 +89,14 @@ public CompletableFuture add(SessionScheduleAddParams * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture addCron(SessionScheduleAddCronParams params) { + CompletableFuture addCron(SessionScheduleAddCronParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.schedule.addCron", _p, SessionScheduleAddCronResult.class); + return caller.invoke("session.schedule.addCron", _p, ScheduleAddResult.class); } /** - * Register an absolute-time scheduled prompt. + * Registers an absolute-time scheduled prompt. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -105,14 +105,14 @@ public CompletableFuture addCron(SessionScheduleAd * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture addAt(SessionScheduleAddAtParams params) { + CompletableFuture addAt(SessionScheduleAddAtParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.schedule.addAt", _p, SessionScheduleAddAtResult.class); + return caller.invoke("session.schedule.addAt", _p, ScheduleAddResult.class); } /** - * Register a self-paced scheduled prompt. + * Registers a self-paced scheduled prompt. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -121,14 +121,14 @@ public CompletableFuture addAt(SessionScheduleAddAtP * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture addSelfPaced(SessionScheduleAddSelfPacedParams params) { + CompletableFuture addSelfPaced(SessionScheduleAddSelfPacedParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.schedule.addSelfPaced", _p, SessionScheduleAddSelfPacedResult.class); + return caller.invoke("session.schedule.addSelfPaced", _p, ScheduleAddResult.class); } /** - * Re-arm a self-paced scheduled prompt. + * Re-arms an active self-paced scheduled prompt. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -137,14 +137,14 @@ public CompletableFuture addSelfPaced(Session * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture rearmSelfPaced(SessionScheduleRearmSelfPacedParams params) { + CompletableFuture rearmSelfPaced(SessionScheduleRearmSelfPacedParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.schedule.rearmSelfPaced", _p, SessionScheduleRearmSelfPacedResult.class); + return caller.invoke("session.schedule.rearmSelfPaced", _p, ScheduleAddResult.class); } /** - * Identifier of the scheduled prompt to remove. + * Removes a scheduled prompt by id. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleHasSelfPacedParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleHasSelfPacedParams.java index 7eb31df7f6..134a381e3b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleHasSelfPacedParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleHasSelfPacedParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionScheduleHasSelfPacedParams( +record SessionScheduleHasSelfPacedParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleHydrateParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleHydrateParams.java index 32ec85c7d4..6eda2a1b32 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleHydrateParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleHydrateParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionScheduleHydrateParams( +record SessionScheduleHydrateParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleRearmSelfPacedParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleRearmSelfPacedParams.java index d1999311c2..f633a98626 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleRearmSelfPacedParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleRearmSelfPacedParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionScheduleRearmSelfPacedParams( +record SessionScheduleRearmSelfPacedParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Id of the self-paced scheduled prompt. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleRearmSelfPacedResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleRearmSelfPacedResult.java deleted file mode 100644 index 0280fbaccb..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionScheduleRearmSelfPacedResult.java +++ /dev/null @@ -1,32 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; -import javax.annotation.processing.Generated; - -/** - * Result of registering or re-arming a scheduled prompt. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ -@CopilotExperimental -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record SessionScheduleRearmSelfPacedResult( - /** The registered or updated schedule entry. */ - @JsonProperty("entry") ScheduleEntry entry, - /** User-facing validation error, when registration failed. */ - @JsonProperty("error") String error -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSendSystemNotificationParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSendSystemNotificationParams.java index 762fe6ef39..dbb8120345 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSendSystemNotificationParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSendSystemNotificationParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionSendSystemNotificationParams( +record SessionSendSystemNotificationParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Notification text to deliver to the model. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsApi.java index dd4acfdb53..f07d028f93 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsApi.java @@ -17,7 +17,7 @@ * @since 1.0.0 */ @javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class SessionSettingsApi { +final class SessionSettingsApi { private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; @@ -31,18 +31,18 @@ public final class SessionSettingsApi { } /** - * Identifies the target session. + * Returns a redacted snapshot of session runtime settings, with secrets and raw feature flags excluded. Internal: the runtime settings shape is a runtime-internal surface and is deliberately kept out of the public SDK, because consumers should not depend on the runtime's internal settings layout. It remains callable in-process and is expected to be reworked as the runtime internals are consolidated. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture snapshot() { - return caller.invoke("session.settings.snapshot", java.util.Map.of("sessionId", this.sessionId), SessionSettingsSnapshotResult.class); + CompletableFuture snapshot() { + return caller.invoke("session.settings.snapshot", java.util.Map.of("sessionId", this.sessionId), SessionSettingsSnapshot.class); } /** - * Named Rust-owned settings predicate to evaluate for this session. + * Evaluates a named Rust-owned settings predicate without exposing raw feature flags. Internal: the raw feature-flag names and composition are runtime-internal, so this predicate-evaluation helper is kept out of the public SDK surface and is callable in-process only. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -51,7 +51,7 @@ public CompletableFuture snapshot() { * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture evaluatePredicate(SessionSettingsEvaluatePredicateParams params) { + CompletableFuture evaluatePredicate(SessionSettingsEvaluatePredicateParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); return caller.invoke("session.settings.evaluatePredicate", _p, SessionSettingsEvaluatePredicateResult.class); diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsBuiltInToolAvailabilitySnapshot.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsBuiltInToolAvailabilitySnapshot.java index 8a714d5ccb..a2bf7aac12 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsBuiltInToolAvailabilitySnapshot.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsBuiltInToolAvailabilitySnapshot.java @@ -20,7 +20,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionSettingsBuiltInToolAvailabilitySnapshot( +record SessionSettingsBuiltInToolAvailabilitySnapshot( /** Whether the report-progress tool is available. */ @JsonProperty("reportProgress") Boolean reportProgress, /** Whether the create-pull-request tool is available. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsEvaluatePredicateParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsEvaluatePredicateParams.java index a7c1663e28..850f657497 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsEvaluatePredicateParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsEvaluatePredicateParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionSettingsEvaluatePredicateParams( +record SessionSettingsEvaluatePredicateParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Predicate name. The runtime owns the raw feature-flag names and composition logic. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsEvaluatePredicateResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsEvaluatePredicateResult.java index 3ab56b8ca6..4fd5a005fd 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsEvaluatePredicateResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsEvaluatePredicateResult.java @@ -10,20 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Result of evaluating a Rust-owned settings predicate. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionSettingsEvaluatePredicateResult( +record SessionSettingsEvaluatePredicateResult( /** Whether the named settings predicate evaluated to enabled. */ @JsonProperty("enabled") Boolean enabled ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsJobSnapshot.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsJobSnapshot.java index 8a807e391c..57a6e9115d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsJobSnapshot.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsJobSnapshot.java @@ -20,7 +20,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionSettingsJobSnapshot( +record SessionSettingsJobSnapshot( /** GitHub Actions event type for the job. */ @JsonProperty("eventType") String eventType, /** Whether this is the workflow's trigger job. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsModelSnapshot.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsModelSnapshot.java index e1eb54a251..04ea8c5b9b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsModelSnapshot.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsModelSnapshot.java @@ -20,7 +20,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionSettingsModelSnapshot( +record SessionSettingsModelSnapshot( /** Selected model identifier. */ @JsonProperty("model") String model, /** Default reasoning effort for the selected model. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsOnlineEvaluationSnapshot.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsOnlineEvaluationSnapshot.java index 999e1a2cbf..458f15f200 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsOnlineEvaluationSnapshot.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsOnlineEvaluationSnapshot.java @@ -20,7 +20,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionSettingsOnlineEvaluationSnapshot( +record SessionSettingsOnlineEvaluationSnapshot( /** Whether online evaluation is disabled. */ @JsonProperty("disableOnlineEvaluation") Boolean disableOnlineEvaluation, /** Whether online-evaluation output-file generation is enabled. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsPredicateName.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsPredicateName.java index 6c99c214a8..45d49f3e9c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsPredicateName.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsPredicateName.java @@ -15,7 +15,7 @@ * @since 1.0.0 */ @javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum SessionSettingsPredicateName { +enum SessionSettingsPredicateName { /** The {@code securityToolsEnabled} variant. */ SECURITYTOOLSENABLED("securityToolsEnabled"), /** The {@code thirdPartySecurityPromptEnabled} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsRepoSnapshot.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsRepoSnapshot.java index 3651346bd8..3ff0fe4684 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsRepoSnapshot.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsRepoSnapshot.java @@ -20,7 +20,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionSettingsRepoSnapshot( +record SessionSettingsRepoSnapshot( /** Repository name. */ @JsonProperty("name") String name, /** GitHub repository database ID. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsSnapshotResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsSnapshot.java similarity index 91% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsSnapshotResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsSnapshot.java index 1bcfe166fc..4a347290d3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsSnapshotResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsSnapshot.java @@ -10,20 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Redacted, serializable view of session runtime settings for SDK boundary consumers. Secrets and raw feature flags are intentionally excluded. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionSettingsSnapshotResult( +record SessionSettingsSnapshot( /** Agent runtime version selector copied from the session settings, such as `latest` or a runtime release identifier. */ @JsonProperty("version") String version, /** Name of the SDK client that created the session. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsSnapshotParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsSnapshotParams.java index 8bad931529..23585e8d91 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsSnapshotParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsSnapshotParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionSettingsSnapshotParams( +record SessionSettingsSnapshotParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsValidationSnapshot.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsValidationSnapshot.java index 314cc5b2b4..cea60b6119 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsValidationSnapshot.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSettingsValidationSnapshot.java @@ -20,7 +20,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionSettingsValidationSnapshot( +record SessionSettingsValidationSnapshot( /** General validation timeout budget in seconds. */ @JsonProperty("timeout") Double timeout, /** Dependabot validation timeout budget in seconds. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellApi.java index b96ac87ea7..6d3187ef75 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellApi.java @@ -31,7 +31,7 @@ public final class SessionShellApi { } /** - * Shell command to run, with optional working directory and timeout in milliseconds. + * 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. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture exec(SessionShellExecParams par } /** - * Identifier of a process previously returned by "shell.exec" and the signal to send. + * Sends a signal to a shell process previously started via "shell.exec". The signal targets the command's whole process group (POSIX) or job object (Windows), so descendants still in that group are signalled too, not just the direct child. On POSIX a descendant that moved itself into a new session or process group (for example via "setsid") is no longer in the signalled group and survives. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture kill(SessionShellKillParams par } /** - * User-requested shell command and cancellation handle. + * Executes a user-requested shell command through the session runtime. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -79,7 +79,7 @@ public CompletableFuture executeUserRequ } /** - * User-requested shell execution cancellation handle. + * Cancels a user-requested shell command by request ID. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellExecParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellExecParams.java index 5ef68032d2..61e8406a6e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellExecParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellExecParams.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * 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. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellExecResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellExecResult.java index 145d5cbb88..82755320db 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellExecResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionShellExecResult.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * 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. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -24,7 +24,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record SessionShellExecResult( - /** Unique identifier for tracking streamed output */ + /** Identifier usable with shell.kill while the process is running */ @JsonProperty("processId") String processId ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSkillsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSkillsApi.java index f6cceb98b9..e21c294b31 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSkillsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSkillsApi.java @@ -31,7 +31,7 @@ public final class SessionSkillsApi { } /** - * Identifies the target session. + * Lists skills available to the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture list() { } /** - * Identifies the target session. + * Returns the skills that have been invoked during this session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -53,7 +53,7 @@ public CompletableFuture getInvoked() { } /** - * Name of the skill to enable for the session. + * Enables a skill for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -69,7 +69,7 @@ public CompletableFuture enable(SessionSkillsEnableParams params) { } /** - * Name of the skill to disable for the session. + * Disables a skill for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -85,7 +85,7 @@ public CompletableFuture disable(SessionSkillsDisableParams params) { } /** - * Identifies the target session. + * Reloads skill definitions for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -96,7 +96,7 @@ public CompletableFuture reload() { } /** - * Identifies the target session. + * Ensures the session's skill definitions have been loaded from disk. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java index c6ca1335de..9e78691586 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java @@ -31,7 +31,7 @@ public final class SessionTasksApi { } /** - * Agent type, prompt, name, and optional description and model override for the new task. + * Starts a background agent task in the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture startAgent(SessionTasksSt } /** - * Identifies the target session. + * Lists background tasks tracked by the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -58,7 +58,7 @@ public CompletableFuture list() { } /** - * Registers or reclaims a client-owned task. + * Registers a client-owned task, or reclaims an orphaned task belonging to the same extension principal. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture register(SessionTasksRegist } /** - * Updates a client-owned task. + * Publishes generic progress or a terminal outcome for a client-owned task. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -90,7 +90,7 @@ public CompletableFuture update(SessionTasksUpdatePara } /** - * Identifies the target session. + * Refreshes metadata for any detached background shells the runtime knows about. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -101,7 +101,7 @@ public CompletableFuture refresh() { } /** - * Identifies the target session. + * Waits for all in-flight background tasks and any follow-up turns to settle. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -112,7 +112,7 @@ public CompletableFuture waitForPending() { } /** - * Identifier of the background task to fetch progress for. + * Returns progress information for a background task by ID. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -128,7 +128,7 @@ public CompletableFuture getProgress(SessionTasks } /** - * Identifies the target session. + * Returns the first sync-waiting task that can currently be promoted to background mode. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -139,7 +139,7 @@ public CompletableFuture getCurrentPromo } /** - * Identifier of the task to promote to background mode. + * Promotes an eligible synchronously-waited task so it continues running in the background. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -155,7 +155,7 @@ public CompletableFuture promoteToBackgro } /** - * Identifies the target session. + * Atomically promotes the first promotable sync-waiting task to background mode and returns it. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -166,7 +166,7 @@ public CompletableFuture promoteCu } /** - * Identifier of the background task to cancel. + * Cancels a background task. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -182,7 +182,7 @@ public CompletableFuture cancel(SessionTasksCancelPara } /** - * Identifier of the completed or cancelled task to remove from tracking. + * Removes a completed or cancelled background task from tracking. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -198,7 +198,7 @@ public CompletableFuture remove(SessionTasksRemovePara } /** - * Identifier of the target agent task, message content, and optional sender agent ID. + * Sends a message to a background agent task. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTelemetryApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTelemetryApi.java index 42589661ef..d74f8366da 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTelemetryApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTelemetryApi.java @@ -31,7 +31,7 @@ public final class SessionTelemetryApi { } /** - * Identifies the target session. + * Gets the telemetry engagement ID currently associated with the session, when available. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture getEngagementId( } /** - * Feature override key/value pairs to attach to subsequent telemetry events from this session. + * Sets feature override key/value pairs to attach to subsequent telemetry events for the session. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsApi.java index 62593a3588..f8b4e32601 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionToolsApi.java @@ -31,7 +31,7 @@ public final class SessionToolsApi { } /** - * A tool name and arguments to execute through the session's native invocation pipeline. + * Executes one tool from the session's currently offered tool set through the native invocation pipeline. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture execute(SessionToolsExecuteParams params) { } /** - * Options controlling how Rust-owned built-in tool descriptors are materialized. + * Returns the Rust-owned built-in tool descriptors used to construct the session's offered tool set. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture getBuiltinDesc } /** - * Task-completion tool arguments and final result used to build a label-safe session event payload. + * Projects a completed task_complete tool call into its label-safe session event payload. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -79,7 +79,7 @@ public CompletableFuture taskCompleteEv } /** - * Pending external tool call request ID, with the tool result or an error describing why it failed. + * Provides the result for a pending external tool call. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -95,7 +95,7 @@ public CompletableFuture handlePendingT } /** - * Identifies the target session. + * Resolves, builds, and validates the runtime tool list for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -106,7 +106,7 @@ public CompletableFuture initializeAndValidate() { } /** - * Identifies the target session. + * Returns lightweight metadata for the session's currently initialized tools. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -117,7 +117,7 @@ public CompletableFuture getCurrentMetadat } /** - * Complete externally implemented tool list for the calling connection. An empty list removes every tool previously supplied by that connection. + * Atomically replaces the complete externally implemented tool list supplied by the calling connection. Built-in, MCP/plugin, extension-discovered, subagent, and tools supplied by other connections remain unchanged. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -133,7 +133,7 @@ public CompletableFuture set(SessionToolsSetParams params) { } /** - * Subagent settings to apply to the current session + * Sets the current session's live subagent settings override, which takes precedence over persisted user settings until cleared. Persisted user settings remain the source of truth for future sessions. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiApi.java index b3e16d7c27..5187b51774 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiApi.java @@ -31,7 +31,7 @@ public final class SessionUiApi { } /** - * Transient question to answer without adding it to conversation history. + * Runs a transient no-tools model query against the current conversation context. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture ephemeralQuery(SessionUi } /** - * Prompt message and JSON schema describing the form fields to elicit from the user. + * Requests structured input from a UI-capable client. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -63,7 +63,7 @@ public CompletableFuture elicitation(SessionUiElicit } /** - * Pending elicitation request ID and the user's response (accept/decline/cancel + form values). + * Provides the user response for a pending elicitation request. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -79,7 +79,23 @@ public CompletableFuture handlePendingE } /** - * Request ID of a pending `user_input.requested` event and the user's response. + * 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. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture handleHumanAskUser(SessionUiHandleHumanAskUserParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.ui.handleHumanAskUser", _p, UIElicitationResult.class); + } + + /** + * Resolves a pending `user_input.requested` event with the user's response. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -95,7 +111,23 @@ public CompletableFuture handlePendingUse } /** - * Request ID of a pending `sampling.requested` event and an optional sampling result payload (omit to reject). + * Resolves a pending `user_input.requested` event after direct interaction in the trusted in-process client. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture handleHumanUserInput(SessionUiHandleHumanUserInputParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.ui.handleHumanUserInput", _p, UIHandlePendingResult.class); + } + + /** + * Resolves a pending `sampling.requested` event with a sampling result, or rejects it. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -111,7 +143,7 @@ public CompletableFuture handlePendingSamp } /** - * Request ID of a pending `auto_mode_switch.requested` event and the user's response. + * Resolves a pending `auto_mode_switch.requested` event with the user's accept/decline decision. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -127,7 +159,7 @@ public CompletableFuture handlePendi } /** - * Request ID of a pending `session_limits_exhausted.requested` event and the user's selected limit action. + * Resolves a pending `session_limits_exhausted.requested` event with the user's selected limit action. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -143,7 +175,7 @@ public CompletableFuture han } /** - * Request ID of a pending `exit_plan_mode.requested` event and the user's response. + * Resolves a pending `exit_plan_mode.requested` event with the user's response. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -159,7 +191,23 @@ public CompletableFuture handlePending } /** - * Identifies the target session. + * Resolves a pending `exit_plan_mode.requested` event after direct interaction in the trusted in-process client. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + CompletableFuture handleHumanExitPlanMode(SessionUiHandleHumanExitPlanModeParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.ui.handleHumanExitPlanMode", _p, UIHandlePendingResult.class); + } + + /** + * Registers an in-process handler for auto-mode-switch requests so the server bridge skips dispatch. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -170,7 +218,7 @@ public CompletableFuture reg } /** - * Opaque handle previously returned by `registerDirectAutoModeSwitchHandler` to release. + * Unregisters a previously-registered in-process auto-mode-switch handler by its opaque handle. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanAskUserParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanAskUserParams.java new file mode 100644 index 0000000000..de9e0419e7 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanAskUserParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Pending elicitation request ID and the user's response (accept/decline/cancel + form values). + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionUiHandleHumanAskUserParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The unique request ID from the elicitation.requested event */ + @JsonProperty("requestId") String requestId, + /** The elicitation response (accept with form values, decline, or cancel) */ + @JsonProperty("result") UIElicitationResponse result +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanExitPlanModeParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanExitPlanModeParams.java new file mode 100644 index 0000000000..aab4e6d7cb --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanExitPlanModeParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Request ID of a pending `exit_plan_mode.requested` event and the user's response. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionUiHandleHumanExitPlanModeParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The unique request ID from the exit_plan_mode.requested event */ + @JsonProperty("requestId") String requestId, + /** User response for a pending exit-plan-mode request, with approval state, selected action, auto-approve flag, and feedback. */ + @JsonProperty("response") UIExitPlanModeResponse response +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanUserInputParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanUserInputParams.java new file mode 100644 index 0000000000..45629b962d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUiHandleHumanUserInputParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Request ID of a pending `user_input.requested` event and the user's response. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +record SessionUiHandleHumanUserInputParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** The unique request ID from the user_input.requested event */ + @JsonProperty("requestId") String requestId, + /** User response for a pending user-input request, with answer text and whether it was typed freeform. */ + @JsonProperty("response") UIUserInputResponse response +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageApi.java index 16ded9d6b6..159fc0993c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionUsageApi.java @@ -29,7 +29,7 @@ public final class SessionUsageApi { } /** - * Identifies the target session. + * Gets accumulated usage metrics for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionVisibilityApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionVisibilityApi.java index 54f38c2614..b942828e10 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionVisibilityApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionVisibilityApi.java @@ -31,7 +31,7 @@ public final class SessionVisibilityApi { } /** - * Identifies the target session. + * Returns the session's current Mission Control sharing status and shareable GitHub URL. Reflects whether the synced session is visible to repository readers ("repo") or restricted to its creator and collaborators ("unshared"). * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture get() { } /** - * Desired sharing status for the session. + * Sets the session's Mission Control sharing status, controlling whether the synced session is visible to repository readers. Returns the effective status and shareable GitHub URL after the change. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowApi.java index adffbd7302..e746033b39 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowApi.java @@ -35,7 +35,7 @@ public final class SessionWorkflowApi { } /** - * Parameters for invoking a registered workflow. + * Runs a registered dynamic workflow by name at the top level. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -51,7 +51,7 @@ public CompletableFuture run(SessionWorkflowRunParams } /** - * Parameters for resuming a workflow run from its persisted identity. + * Resumes a dynamic workflow run using its persisted name, arguments, journal, and accounting. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -67,7 +67,7 @@ public CompletableFuture resume(SessionWorkflowResu } /** - * Internal parameters for invoking a registered workflow from a tool. + * Internal tool-originated dynamic workflow invocation. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -76,14 +76,14 @@ public CompletableFuture resume(SessionWorkflowResu * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture runFromTool(SessionWorkflowRunFromToolParams params) { + CompletableFuture runFromTool(SessionWorkflowRunFromToolParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.workflow.runFromTool", _p, SessionWorkflowRunFromToolResult.class); + return caller.invoke("session.workflow.runFromTool", _p, WorkflowRunResult.class); } /** - * Internal parameters for resuming a workflow run from a tool. + * Internal tool-originated dynamic workflow resume. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -92,14 +92,14 @@ public CompletableFuture runFromTool(SessionWo * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture resumeFromTool(SessionWorkflowResumeFromToolParams params) { + CompletableFuture resumeFromTool(SessionWorkflowResumeFromToolParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.workflow.resumeFromTool", _p, SessionWorkflowResumeFromToolResult.class); + return caller.invoke("session.workflow.resumeFromTool", _p, WorkflowResumeResult.class); } /** - * Parameters for retrieving a workflow run. + * Gets the current or settled envelope for a dynamic workflow run. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -115,7 +115,7 @@ public CompletableFuture getRun(SessionWorkflowGetR } /** - * Parameters for paging workflow runs. + * Lists durable dynamic workflow runs for this session in creation order. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -131,7 +131,7 @@ public CompletableFuture listRuns(SessionWorkflow } /** - * Parameters for retrieving a workflow run. + * Gets durable and live observability detail for one dynamic workflow run. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -147,7 +147,7 @@ public CompletableFuture getRunDetail(Session } /** - * Parameters for paging workflow progress. + * Pages durable progress for one dynamic workflow run. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -163,7 +163,7 @@ public CompletableFuture getRunProgress(Ses } /** - * Parameters for cancelling a workflow run. + * Requests cancellation of a dynamic workflow run and returns its run envelope. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -179,7 +179,7 @@ public CompletableFuture cancel(SessionWorkflowCanc } /** - * Parameters for pausing a running workflow. + * Pauses a running dynamic workflow and returns its settled run envelope. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -195,7 +195,7 @@ public CompletableFuture pause(SessionWorkflowPauseP } /** - * Parameters for an owned durable pause checkpoint. + * Atomically pauses an owned dynamic workflow attempt at a durable checkpoint. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -204,14 +204,14 @@ public CompletableFuture pause(SessionWorkflowPauseP * @since 1.0.0 */ @CopilotExperimental - public CompletableFuture pauseAtCheckpoint(SessionWorkflowPauseAtCheckpointParams params) { + CompletableFuture pauseAtCheckpoint(SessionWorkflowPauseAtCheckpointParams params) { com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); _p.put("sessionId", this.sessionId); - return caller.invoke("session.workflow.pauseAtCheckpoint", _p, SessionWorkflowPauseAtCheckpointResult.class); + return caller.invoke("session.workflow.pauseAtCheckpoint", _p, WorkflowPauseCheckpointResult.class); } /** - * Parameters for recording workflow progress. + * Records a batch of ordered dynamic workflow progress lines. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -227,7 +227,7 @@ public CompletableFuture log(SessionWorkflowLogParams params) { } /** - * Parameters for one workflow-scoped subagent call. + * Runs one dynamic-workflow-scoped subagent and returns its result. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowJournalApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowJournalApi.java index 0909931439..70bc1b47e2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowJournalApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowJournalApi.java @@ -31,7 +31,7 @@ public final class SessionWorkflowJournalApi { } /** - * Parameters for reading a workflow journal entry. + * Reads a memoized dynamic workflow journal entry. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -47,7 +47,7 @@ public CompletableFuture get(SessionWorkflowJou } /** - * Parameters for storing a workflow journal entry. + * Stores a memoized dynamic workflow journal entry. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowPauseAtCheckpointParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowPauseAtCheckpointParams.java index 5000a5c4d8..5ca48e5ee6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowPauseAtCheckpointParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowPauseAtCheckpointParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionWorkflowPauseAtCheckpointParams( +record SessionWorkflowPauseAtCheckpointParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Workflow run identifier. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowResumeFromToolParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowResumeFromToolParams.java index c9630f6ede..84e0eca0fb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowResumeFromToolParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowResumeFromToolParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionWorkflowResumeFromToolParams( +record SessionWorkflowResumeFromToolParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Workflow run identifier. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowRunFromToolParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowRunFromToolParams.java index 8fc9661ea4..d68976fb76 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowRunFromToolParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowRunFromToolParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionWorkflowRunFromToolParams( +record SessionWorkflowRunFromToolParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Registered workflow name. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowRunFromToolResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowRunFromToolResult.java deleted file mode 100644 index d5ab4d5e6e..0000000000 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowRunFromToolResult.java +++ /dev/null @@ -1,46 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; -import javax.annotation.processing.Generated; - -/** - * Complete current or terminal workflow run envelope. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ -@CopilotExperimental -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record SessionWorkflowRunFromToolResult( - /** Workflow run identifier. */ - @JsonProperty("runId") String runId, - /** One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. */ - @JsonProperty("attempt") Long attempt, - /** Current or terminal workflow run status. */ - @JsonProperty("status") WorkflowRunStatus status, - /** Completed workflow result. */ - @JsonProperty("result") Object result, - /** Error message for an errored run. */ - @JsonProperty("error") String error, - /** Machine-readable failure details for a halted or errored run. */ - @JsonProperty("failure") Object failure, - /** Reason for a halted or cancelled run. */ - @JsonProperty("reason") String reason, - /** Partial journal and progress snapshot for a halted, cancelled, or errored run. */ - @JsonProperty("snapshot") Object snapshot, - /** Structured pause initiator metadata for a paused attempt. */ - @JsonProperty("pauseInfo") Object pauseInfo -) { -} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkspacesApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkspacesApi.java index 71a3d1ea1b..889f4a0de2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkspacesApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkspacesApi.java @@ -31,7 +31,7 @@ public final class SessionWorkspacesApi { } /** - * Identifies the target session. + * Gets current workspace metadata for the session. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -42,7 +42,7 @@ public CompletableFuture getWorkspace() { } /** - * Workspace metadata fields to update. + * Updates workspace metadata for a local session and returns the refreshed workspace. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -58,7 +58,7 @@ public CompletableFuture updateMetadata(S } /** - * Optional session context used when creating a local workspace. + * Ensures a local session workspace exists and returns it. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -74,7 +74,7 @@ public CompletableFuture ensure(SessionWorkspaces } /** - * Identifies the target session. + * Lists files stored in the session workspace files directory. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -85,7 +85,7 @@ public CompletableFuture listFiles() { } /** - * Relative path of the workspace file to read. + * Reads a file from the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -101,7 +101,7 @@ public CompletableFuture readFile(SessionWorksp } /** - * Relative path and UTF-8 content for the workspace file to create or overwrite. + * Creates or overwrites a file in the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -117,7 +117,7 @@ public CompletableFuture createFile(SessionWorkspacesCreateFileParams para } /** - * Relative path of the workspace file or directory to inspect. + * Returns metadata for a file or directory in the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -133,7 +133,7 @@ public CompletableFuture statFile(SessionWorksp } /** - * Directory to create within the session workspace files directory. + * Creates a directory in the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -149,7 +149,7 @@ public CompletableFuture createDirectory(SessionWorkspacesCreateDirectoryP } /** - * File or directory to remove from the session workspace files directory. + * Removes a file or directory from the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -165,7 +165,7 @@ public CompletableFuture removePath(SessionWorkspacesRemovePathParams para } /** - * Source and destination paths for a rename within the session workspace files directory. + * Renames a file or directory within the session workspace files directory. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -181,7 +181,7 @@ public CompletableFuture renamePath(SessionWorkspacesRenamePathParams para } /** - * Identifies the target session. + * Lists workspace checkpoints in chronological order. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -192,7 +192,7 @@ public CompletableFuture listCheckpoints } /** - * Checkpoint number to read. + * Reads the content of a workspace checkpoint by number. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -208,7 +208,7 @@ public CompletableFuture readCheckpoint(S } /** - * Compaction summary checkpoint to persist. + * Adds a compaction summary checkpoint to the local session workspace. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -224,7 +224,7 @@ public CompletableFuture addSummary(SessionWo } /** - * Rollback point for local workspace summaries. + * Truncates local workspace compaction summaries after a rollback. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -240,7 +240,7 @@ public CompletableFuture truncateSumma } /** - * Identifies the target session. + * Reads the autopilot objective state file from the local session workspace. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -251,7 +251,7 @@ public CompletableFuture readAuto } /** - * Autopilot objective file content to persist. + * Writes the autopilot objective state file in the local session workspace. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -267,7 +267,7 @@ public CompletableFuture writeAu } /** - * Identifies the target session. + * Deletes the autopilot objective state file from the local session workspace. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -278,7 +278,7 @@ public CompletableFuture delete } /** - * Identifies the target session. + * Checks whether the local session workspace has an autopilot objective state file. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -289,7 +289,7 @@ public CompletableFuture autopi } /** - * Pasted content to save as a UTF-8 file in the session workspace. + * Saves pasted content as a UTF-8 file in the session workspace. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. @@ -305,7 +305,7 @@ public CompletableFuture saveLargePaste(S } /** - * Parameters for computing a workspace diff. + * Computes a diff for the session workspace. Never rejects for a busy session: a `session`-mode diff that cannot read the session's file-change captures falls back to an unstaged git diff with `isFallback: true` and reports why in `unavailableReason`. *

* Note: the {@code sessionId} field in the params record is overridden * by the session-scoped wrapper; any value provided is ignored. diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsConfigureSessionExtensionsParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsConfigureSessionExtensionsParams.java index 83d2d9c61c..e39e43456a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsConfigureSessionExtensionsParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsConfigureSessionExtensionsParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionsConfigureSessionExtensionsParams( +record SessionsConfigureSessionExtensionsParams( /** Session to attach the extension controller delegate to. */ @JsonProperty("sessionId") String sessionId, /** In-process ExtensionController delegate (CLI-only optimization). Marked internal: this field is excluded from the public SDK surface. The post-SDK extension surface exposes list/enable/disable/reload via dedicated RPCs served by the runtime. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsDeleteParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsDeleteParams.java index 788811e348..ee7d6e508d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsDeleteParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsDeleteParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionsDeleteParams( +record SessionsDeleteParams( /** Session ID to delete */ @JsonProperty("sessionId") String sessionId, /** Internal resolved session directory path to delete */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetBoardEntryCountParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetBoardEntryCountParams.java index 28ea6d8c24..0710387f91 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetBoardEntryCountParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetBoardEntryCountParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionsGetBoardEntryCountParams( +record SessionsGetBoardEntryCountParams( /** Session ID whose board entry count should be returned. */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetBoardEntryCountResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetBoardEntryCountResult.java index b0c70169ed..fe0412c114 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetBoardEntryCountResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetBoardEntryCountResult.java @@ -10,20 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Dynamic-context board entry count, when available. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionsGetBoardEntryCountResult( +record SessionsGetBoardEntryCountResult( /** Board entry count, when available. */ @JsonProperty("count") Long count ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetEventFilePathParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetEventFilePathParams.java index 0b8c72e581..2953a1cdbd 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetEventFilePathParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetEventFilePathParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionsGetEventFilePathParams( +record SessionsGetEventFilePathParams( /** Session ID whose event-log file path to compute */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetEventFilePathResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetEventFilePathResult.java index c403264a9d..cea6c10f29 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetEventFilePathResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetEventFilePathResult.java @@ -10,20 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Absolute path to the session's events.jsonl file on disk. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionsGetEventFilePathResult( +record SessionsGetEventFilePathResult( /** Absolute path to the session's events.jsonl file */ @JsonProperty("filePath") String filePath ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetMetadataParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetMetadataParams.java index cfa2e6326e..0e8dfe14a1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetMetadataParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetMetadataParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionsGetMetadataParams( +record SessionsGetMetadataParams( /** Session ID to inspect */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetMetadataResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetMetadataResult.java index 6546b00fe0..54571785fe 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetMetadataResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetMetadataResult.java @@ -10,20 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Persisted local session metadata when the session exists. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionsGetMetadataResult( +record SessionsGetMetadataResult( /** Local session metadata, omitted when the session does not exist. */ @JsonProperty("session") LocalSessionMetadataValue session ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetPersistedRemoteSteerableParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetPersistedRemoteSteerableParams.java index 976efd6751..98622cf322 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetPersistedRemoteSteerableParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetPersistedRemoteSteerableParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionsGetPersistedRemoteSteerableParams( +record SessionsGetPersistedRemoteSteerableParams( /** Session ID to look up the persisted remote-steerable flag for */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetPersistedRemoteSteerableResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetPersistedRemoteSteerableResult.java index 97127d8560..4ee41920f3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetPersistedRemoteSteerableResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsGetPersistedRemoteSteerableResult.java @@ -10,20 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * The session's persisted remote-steerable flag, or omitted when no value has been persisted. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionsGetPersistedRemoteSteerableResult( +record SessionsGetPersistedRemoteSteerableResult( /** The session's persisted remote-steerable flag if recorded; omitted when no value has been persisted */ @JsonProperty("remoteSteerable") Boolean remoteSteerable ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsListNonEmptySessionIdsParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsListNonEmptySessionIdsParams.java index 4e453121bc..a8de88c58b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsListNonEmptySessionIdsParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsListNonEmptySessionIdsParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionsListNonEmptySessionIdsParams( +record SessionsListNonEmptySessionIdsParams( /** Maximum number of session IDs to return. */ @JsonProperty("limit") Long limit ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsListNonEmptySessionIdsResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsListNonEmptySessionIdsResult.java index 51cc266e63..1a20c17a8d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsListNonEmptySessionIdsResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsListNonEmptySessionIdsResult.java @@ -10,21 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; /** * Recent local session IDs that contain user-visible history. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionsListNonEmptySessionIdsResult( +record SessionsListNonEmptySessionIdsResult( /** Session IDs ordered newest-first. */ @JsonProperty("sessionIds") List sessionIds ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderDescriptor.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderDescriptor.java index a678b95fa3..659da543b3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderDescriptor.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderDescriptor.java @@ -20,7 +20,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SkillProviderDescriptor( +record SkillProviderDescriptor( /** Invocation and display name. */ @JsonProperty("name") String name, /** Description used in skill catalogs without fetching content. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListParams.java index e11fcf0fbd..5e351adc89 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SkillProviderListParams( +record SkillProviderListParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListResult.java index 228ec9240b..aac59a687a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListResult.java @@ -10,21 +10,18 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import java.util.List; import javax.annotation.processing.Generated; /** * Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to 1024 descriptors and 1 MiB of aggregate metadata. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SkillProviderListResult( +record SkillProviderListResult( /** Skill descriptors in provider order. Invocation names must be unique under case-insensitive comparison. */ @JsonProperty("skills") List skills ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadParams.java index 895078384b..8da60c6e0f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadParams.java @@ -23,7 +23,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SkillProviderReadParams( +record SkillProviderReadParams( /** Target session identifier */ @JsonProperty("sessionId") String sessionId, /** Invocation name of the skill to read. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadResult.java index 0f17696565..96bb95206b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadResult.java @@ -10,20 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Complete text-only SKILL.md content returned by an SDK session's skill provider. Related files and assets are not supported. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SkillProviderReadResult( +record SkillProviderReadResult( /** Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. */ @JsonProperty("markdown") String markdown ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskKind.java index 413859db20..5db08809c2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskKind.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskKind.java @@ -15,7 +15,7 @@ * @since 1.0.0 */ @javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum TaskKind { +enum TaskKind { /** The {@code agent} variant. */ AGENT("agent"), /** The {@code shell} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UIElicitationResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UIElicitationResult.java new file mode 100644 index 0000000000..7b5dbb2d00 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UIElicitationResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the elicitation response was accepted; false if it was already resolved by another client. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UIElicitationResult( + /** Whether the response was accepted. False if the request was already resolved by another client. */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UIHandlePendingResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UIHandlePendingResult.java new file mode 100644 index 0000000000..a71e3af6b3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/UIHandlePendingResult.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the pending UI request was resolved by this call. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record UIHandlePendingResult( + /** 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. */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkflowPauseCheckpointAction.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkflowPauseCheckpointAction.java index 9c034a66a9..e6a7140479 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkflowPauseCheckpointAction.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkflowPauseCheckpointAction.java @@ -15,7 +15,7 @@ * @since 1.0.0 */ @javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum WorkflowPauseCheckpointAction { +enum WorkflowPauseCheckpointAction { /** The {@code continue} variant. */ CONTINUE("continue"), /** The {@code pause} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowPauseAtCheckpointResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkflowPauseCheckpointResult.java similarity index 74% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowPauseAtCheckpointResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkflowPauseCheckpointResult.java index 9c2a46ab38..653ffa67cb 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowPauseAtCheckpointResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkflowPauseCheckpointResult.java @@ -10,20 +10,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; -/** - * Result for the {@code session.workflow.pauseAtCheckpoint} RPC method. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionWorkflowPauseAtCheckpointResult( +record WorkflowPauseCheckpointResult( /** Whether this execution attempt must pause or may continue. */ @JsonProperty("action") WorkflowPauseCheckpointAction action ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowResumeFromToolResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkflowResumeResult.java similarity index 84% rename from java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowResumeFromToolResult.java rename to java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkflowResumeResult.java index 65dc8bfbfe..e030bde8b4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionWorkflowResumeFromToolResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkflowResumeResult.java @@ -10,20 +10,17 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; import javax.annotation.processing.Generated; /** * Resolved persisted workflow identity and resumed run envelope. * - * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 */ -@CopilotExperimental @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record SessionWorkflowResumeFromToolResult( +public record WorkflowResumeResult( /** Persisted workflow name resolved for the resumed run. */ @JsonProperty("workflowName") String workflowName, /** Terminal resumed run envelope. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkflowToolRunOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkflowToolRunOptions.java index 3a0f5099cf..6211f5a376 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkflowToolRunOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkflowToolRunOptions.java @@ -24,7 +24,7 @@ @javax.annotation.processing.Generated("copilot-sdk-codegen") @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public record WorkflowToolRunOptions( +record WorkflowToolRunOptions( /** Per-invocation resource ceiling overrides. */ @JsonProperty("limits") WorkflowRunLimits limits, /** Run identifier whose journal and progress should seed this resumed run. */ diff --git a/java/sdk/src/main/java/com/github/copilot/AhpHost.java b/java/sdk/src/main/java/com/github/copilot/AhpHost.java index 1f171552ba..9258289c85 100644 --- a/java/sdk/src/main/java/com/github/copilot/AhpHost.java +++ b/java/sdk/src/main/java/com/github/copilot/AhpHost.java @@ -38,7 +38,8 @@ public String getHostId() { /** * Gets the bound address. * - * @return the actual bound WebSocket URL + * @return the actual bound WebSocket URL, or {@code null} without a local + * server */ public String getUrl() { return info.url(); @@ -47,7 +48,7 @@ public String getUrl() { /** * Gets the secret token. * - * @return the token, or {@code null} when authentication is disabled + * @return the token, or {@code null} without an authenticated local server */ public String getToken() { return info.token(); @@ -62,6 +63,15 @@ public Long getPid() { return info.pid(); } + /** + * Gets the GitHub Mission Control environment identity. + * + * @return the environment ID, or {@code null} without a GitHub environment + */ + public String getEnvironmentId() { + return info.environmentId(); + } + /** * Stops the listener and joins runtime cleanup. Every call reaches the runtime, * including concurrent and repeated disposal calls. diff --git a/java/sdk/src/main/java/com/github/copilot/AhpHostManager.java b/java/sdk/src/main/java/com/github/copilot/AhpHostManager.java index ec044bfe54..afd06e8f0e 100644 --- a/java/sdk/src/main/java/com/github/copilot/AhpHostManager.java +++ b/java/sdk/src/main/java/com/github/copilot/AhpHostManager.java @@ -99,10 +99,8 @@ CompletableFuture start(ServerHostApi rpc, AhpHostOptions options) { } var snapshot = host.options; return rpc - .start(new HostStartParams(host.id, snapshot.getHostname(), - snapshot.getPort() == null ? null : snapshot.getPort().longValue(), snapshot.getToken(), - snapshot.getRequireConnectionToken(), snapshot.getCreateSession() != null, - snapshot.getResumeSession() != null)) + .start(new HostStartParams(host.id, snapshot.getLocalServer(), snapshot.getGithubEnvironment(), + snapshot.getCreateSession() != null, snapshot.getResumeSession() != null)) .thenApply(info -> new AhpHost(info, rpc)).whenComplete((result, error) -> { if (error != null) { endHost(host, null); diff --git a/java/sdk/src/main/java/com/github/copilot/AhpHostOptions.java b/java/sdk/src/main/java/com/github/copilot/AhpHostOptions.java index 8c95a5a5e6..2ee29adab0 100644 --- a/java/sdk/src/main/java/com/github/copilot/AhpHostOptions.java +++ b/java/sdk/src/main/java/com/github/copilot/AhpHostOptions.java @@ -4,38 +4,37 @@ package com.github.copilot; +import com.github.copilot.generated.rpc.HostGitHubEnvironmentOptions; +import com.github.copilot.generated.rpc.HostLocalServerOptions; import java.util.concurrent.CompletableFuture; import java.util.function.Function; /** - * Options for an in-process AHP listener. Factories must return the exact + * Options for in-process AHP hosting. Select at least one transport explicitly; + * no local listener is started by default. Factories must return the exact * session registered on the owning client with the requested identity and * settings. Release callbacks end a participation, not the application's * session lifetime. */ @CopilotExperimental public final class AhpHostOptions { - private String hostname; - private Integer port; - private String token; - private Boolean requireConnectionToken; + private HostLocalServerOptions localServer; + private HostGitHubEnvironmentOptions githubEnvironment; private Function> createSession; private Function> resumeSession; private Function> onSessionReleased; private Function> onExit; /** - * Creates options with loopback binding, an ephemeral port, and token - * authentication. + * Creates options without a transport. Set a local server, a GitHub + * environment, or both before starting the host. */ public AhpHostOptions() { } AhpHostOptions(AhpHostOptions options) { - hostname = options.hostname; - port = options.port; - token = options.token; - requireConnectionToken = options.requireConnectionToken; + localServer = options.localServer; + githubEnvironment = options.githubEnvironment; createSession = options.createSession; resumeSession = options.resumeSession; onSessionReleased = options.onSessionReleased; @@ -43,86 +42,44 @@ public AhpHostOptions() { } /** - * Gets the bind hostname. + * Gets the local WebSocket transport settings. * - * @return the hostname, or {@code null} for loopback + * @return the settings, or {@code null} when local hosting is disabled */ - public String getHostname() { - return hostname; + public HostLocalServerOptions getLocalServer() { + return localServer; } /** - * Sets the bind hostname. + * Enables or disables the local WebSocket transport. * - * @param hostname - * bind hostname + * @param localServer + * local settings, or {@code null} to disable local hosting * @return these options */ - public AhpHostOptions setHostname(String hostname) { - this.hostname = hostname; + public AhpHostOptions setLocalServer(HostLocalServerOptions localServer) { + this.localServer = localServer; return this; } /** - * Gets the requested port. + * Gets the GitHub Mission Control transport settings. * - * @return the port, or {@code null} for an ephemeral port + * @return the settings, or {@code null} when GitHub hosting is disabled */ - public Integer getPort() { - return port; + public HostGitHubEnvironmentOptions getGithubEnvironment() { + return githubEnvironment; } /** - * Sets the bind port. + * Enables or disables GitHub Mission Control hosting. * - * @param port - * bind port; zero requests an ephemeral port + * @param githubEnvironment + * settings with a required name and compute ID, or {@code null} * @return these options */ - public AhpHostOptions setPort(Integer port) { - this.port = port; - return this; - } - - /** - * Gets the connection token. - * - * @return the token, or {@code null} to generate one - */ - public String getToken() { - return token; - } - - /** - * Sets the connection token. - * - * @param token - * nonempty connection token - * @return these options - */ - public AhpHostOptions setToken(String token) { - this.token = token; - return this; - } - - /** - * Gets the authentication policy. - * - * @return whether authentication is required; {@code null} means true - */ - public Boolean getRequireConnectionToken() { - return requireConnectionToken; - } - - /** - * Sets the authentication policy. - * - * @param required - * whether to require token authentication - * @return these options - */ - public AhpHostOptions setRequireConnectionToken(Boolean required) { - requireConnectionToken = required; + public AhpHostOptions setGithubEnvironment(HostGitHubEnvironmentOptions githubEnvironment) { + this.githubEnvironment = githubEnvironment; return this; } diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java index ae5c977388..4a6d33c8ce 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java @@ -25,6 +25,7 @@ import java.util.logging.Level; import java.util.logging.Logger; +import com.fasterxml.jackson.databind.JsonNode; import com.github.copilot.ffi.FfiRuntimeHost; import com.github.copilot.ffi.NativeRuntimeLoader; import com.github.copilot.rpc.CopilotClientMode; @@ -39,7 +40,6 @@ import com.github.copilot.rpc.CreateSessionResponse; import com.github.copilot.generated.rpc.SessionOptionsUpdateParams; import com.github.copilot.generated.rpc.SessionInstalledPlugin; -import com.github.copilot.generated.rpc.ConnectResult; import com.github.copilot.generated.rpc.GitHubTelemetryNotification; import com.github.copilot.generated.rpc.ServerRpc; import com.github.copilot.generated.rpc.SessionEventLogRegisterInterestParams; @@ -640,10 +640,22 @@ private Connection startCoreBody() { startNanos); } // Clean up the spawned process if connection setup failed - if (process != null && cleanupCliProcess(process, true)) { - // Cleanup observed an exited child and left its streams open. - // Drain stderr before rpc.close() destroys those streams. - serverManager.awaitStderrReader(); + if (process != null) { + Throwable startupFailure = e; + while (startupFailure instanceof java.util.concurrent.ExecutionException + || startupFailure instanceof CompletionException) { + startupFailure = startupFailure.getCause(); + } + // A broken pipe can precede the child's own exit and trailing stderr. + // Other setup failures must terminate a live server before waiting for EOF. + boolean drainBeforeCleanup = startupFailure instanceof IOException; + if (drainBeforeCleanup) { + serverManager.awaitStderrReader(); + } + cleanupCliProcess(process, true); + if (!drainBeforeCleanup) { + serverManager.awaitStderrReader(); + } } if (rpc != null) { try { @@ -692,11 +704,10 @@ private void verifyProtocolVersion(Connection connection) throws Exception { if (clientInfo != null && !clientInfo.isEmpty()) { connectParams.put("clientInfo", clientInfo); } - var connectResponse = connection.rpc.invoke("connect", connectParams, ConnectResult.class).get(30, + var connectResponse = connection.rpc.invoke("connect", connectParams, JsonNode.class).get(30, TimeUnit.SECONDS); - serverVersion = connectResponse.protocolVersion() != null - ? connectResponse.protocolVersion().intValue() - : null; + var protocolVersion = connectResponse.get("protocolVersion"); + serverVersion = protocolVersion != null && !protocolVersion.isNull() ? protocolVersion.intValue() : null; } catch (Exception e) { // Unwrap CompletionException/ExecutionException to check inner cause Throwable cause = e; @@ -1274,6 +1285,7 @@ public CompletableFuture resumeSession(String sessionId, ResumeS session.setWorkspacePath(response.workspacePath()); session.setCapabilities(response.capabilities()); session.setOpenCanvases(response.openCanvases()); + session.setTranscriptRecovery(response.transcriptRecovery()); // If the server returned a different sessionId than what was requested, // re-key. String returnedId = response.sessionId(); @@ -1805,7 +1817,8 @@ public AutoCloseable onLifecycle(String eventType, SessionLifecycleHandler handl } /** - * Starts an in-process AHP listener owned by this client's current connection. + * Starts in-process AHP hosting owned by this client's current connection. At + * least one of local server or GitHub environment must be selected explicitly. * Application callbacks stay in this SDK process; no host executable is * launched. If the returned future is cancelled, a listener that subsequently * starts is disposed. @@ -1816,10 +1829,15 @@ public AutoCloseable onLifecycle(String eventType, SessionLifecycleHandler handl */ @CopilotExperimental public CompletableFuture startAhpHost(AhpHostOptions options) { + if (options == null || (options.getLocalServer() == null && options.getGithubEnvironment() == null)) { + return CompletableFuture + .failedFuture(new IllegalArgumentException("At least one AHP transport must be configured")); + } + var snapshot = new AhpHostOptions(options); var result = new CompletableFuture(); ensureConnected().thenCompose(connection -> result.isDone() ? CompletableFuture.failedFuture(new java.util.concurrent.CancellationException()) - : ahpHosts.start(connection.serverRpc().host, options)).whenComplete((host, error) -> { + : ahpHosts.start(connection.serverRpc().host, snapshot)).whenComplete((host, error) -> { if (error != null) { result.completeExceptionally(error); } else if (!result.complete(host)) { diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java index eac5d9763c..ddd46aa262 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -47,6 +48,7 @@ import com.github.copilot.generated.rpc.SessionPermissionsHandlePendingPermissionRequestParams; import com.github.copilot.generated.rpc.SessionRpc; import com.github.copilot.generated.rpc.SessionToolsHandlePendingToolCallParams; +import com.github.copilot.generated.rpc.SessionToolsSetParams; import com.github.copilot.generated.rpc.SessionUiElicitationParams; import com.github.copilot.generated.rpc.SessionUiHandlePendingElicitationParams; import com.github.copilot.generated.rpc.UIElicitationResponse; @@ -110,8 +112,11 @@ import com.github.copilot.rpc.SessionStartHookInput; import com.github.copilot.rpc.SessionUiApi; import com.github.copilot.rpc.SessionUiCapabilities; +import com.github.copilot.rpc.SubagentStartHookInput; +import com.github.copilot.rpc.SubagentStopHookInput; import com.github.copilot.rpc.ToolDefinition; import com.github.copilot.rpc.ToolResultObject; +import com.github.copilot.rpc.TranscriptRecoveryReport; import com.github.copilot.rpc.UserInputHandler; import com.github.copilot.rpc.UserInputInvocation; import com.github.copilot.rpc.UserInputRequest; @@ -183,6 +188,7 @@ public final class CopilotSession implements AutoCloseable { */ private volatile String sessionId; private volatile String workspacePath; + private volatile TranscriptRecoveryReport transcriptRecovery; private volatile SessionCapabilities capabilities = new SessionCapabilities(); private final Object openCanvasesLock = new Object(); private final List openCanvases = new ArrayList<>(); @@ -190,7 +196,9 @@ public final class CopilotSession implements AutoCloseable { private final JsonRpcClient rpc; private volatile SessionRpc sessionRpc; private final Set> eventHandlers = new CopyOnWriteArraySet<>(); - private final Map toolHandlers = new ConcurrentHashMap<>(); + private final AtomicReference> toolHandlers = new AtomicReference<>(Map.of()); + private final Object setToolsLock = new Object(); + private CompletableFuture setToolsTail = CompletableFuture.completedFuture(null); private final Map pendingExternalTools = new ConcurrentHashMap<>(); private boolean externalToolsClosed; private final Map commandHandlers = new ConcurrentHashMap<>(); @@ -361,6 +369,19 @@ void setWorkspacePath(String workspacePath) { this.workspacePath = workspacePath; } + /** + * Gets the transcript repair details reported when this session was resumed. + * + * @return repair details, or {@code null} when no repair was reported + */ + public TranscriptRecoveryReport getTranscriptRecovery() { + return transcriptRecovery; + } + + void setTranscriptRecovery(TranscriptRecoveryReport transcriptRecovery) { + this.transcriptRecovery = transcriptRecovery; + } + /** * Gets the capabilities reported by the host for this session. *

@@ -738,6 +759,65 @@ public CompletableFuture sendAndWait(MessageOptions optio return sendAndWait(options, 60000); } + /** + * Replaces this client's externally implemented tools on the live session. + *

+ * This is a complete replacement for the tools supplied by this client only: + * built-in tools, MCP/plugin tools, and tools supplied by other connections are + * not changed. Pass the same {@link ToolDefinition} values accepted by + * {@link com.github.copilot.rpc.SessionConfig#setTools(List)} and + * {@link com.github.copilot.rpc.ResumeSessionConfig#setTools(List)}. Passing an + * empty list removes all tools supplied by this client. + *

+ * The local handlers switch once the runtime accepts the replacement. Tool + * calls already running finish on the handlers that started them. If the + * runtime rejects the replacement, the previous handlers remain unchanged and + * the returned future completes exceptionally. Concurrent replacements on the + * same session are applied in call order. Cancelling the returned future while + * an earlier replacement is still in flight sends nothing; once the request is + * sent, 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. + * + * @param tools + * the complete replacement tool list for this client + * @return a future that completes when the runtime accepts the replacement and + * the local handler registry has been updated + */ + @CopilotExperimental + public CompletableFuture setTools(List tools) { + ensureNotTerminated(); + List toolList = tools != null ? List.copyOf(tools) : List.of(); + Map replacementHandlers = buildToolHandlerMap(toolList); + var params = new SessionToolsSetParams(sessionId, SessionRequestBuilder.toProtocolExternalTools(toolList)); + + var result = new CompletableFuture(); + CompletableFuture operation; + synchronized (setToolsLock) { + operation = setToolsTail.handle((ignored, error) -> null).thenCompose(ignored -> { + if (result.isCancelled()) { + return CompletableFuture.completedFuture(null); + } + return getRpc().tools.set(params).thenRun(() -> toolHandlers.set(replacementHandlers)); + }); + setToolsTail = operation.handle((ignored, error) -> null); + } + + operation.whenComplete((value, error) -> { + if (error != null) { + result.completeExceptionally(error); + } else { + result.complete(null); + } + }); + return result; + } + /** * Sends a prompt using the schema generated for a {@link CopilotResponse} type. * @@ -914,7 +994,8 @@ synchronized void event(SessionEvent event) { * The handler will be invoked for every event in this session, including * assistant messages, tool calls, and session state changes. For type-safe * handling of specific event types, prefer {@link #on(Class, Consumer)} - * instead. + * instead. A handler registered from an event handler does not receive the + * event currently being delivered. * *

* Exception handling: If a handler throws an exception, the error is @@ -1424,12 +1505,18 @@ private void sendMcpAuthResponse(String requestId, McpAuthResult result) { * the list of tool definitions with handlers */ void registerTools(List tools) { - toolHandlers.clear(); - if (tools != null) { - for (ToolDefinition tool : tools) { - toolHandlers.put(tool.name(), tool); - } + toolHandlers.set(buildToolHandlerMap(tools)); + } + + private static Map buildToolHandlerMap(List tools) { + if (tools == null || tools.isEmpty()) { + return Map.of(); + } + Map handlers = new LinkedHashMap<>(); + for (ToolDefinition tool : tools) { + handlers.put(tool.name(), tool); } + return Collections.unmodifiableMap(handlers); } /** @@ -1663,7 +1750,7 @@ public CompletableFuture input(String message, InputOptions options) { * @return the tool definition, or {@code null} if not found */ ToolDefinition getTool(String name) { - return toolHandlers.get(name); + return toolHandlers.get().get(name); } /** @@ -2208,6 +2295,26 @@ CompletableFuture handleHooksInvoke(String hookType, JsonNode input) { return stopResult.thenApply(output -> (Object) output); } break; + case "subagentStart" : + if (hooks.getOnSubagentStart() != null) { + SubagentStartHookInput startInput = MAPPER.treeToValue(input, SubagentStartHookInput.class); + var startResult = hooks.getOnSubagentStart().handle(startInput, invocation); + if (startResult == null) { + return CompletableFuture.completedFuture(null); + } + return startResult.thenApply(output -> (Object) output); + } + break; + case "subagentStop" : + if (hooks.getOnSubagentStop() != null) { + SubagentStopHookInput stopInput = MAPPER.treeToValue(input, SubagentStopHookInput.class); + var stopResult = hooks.getOnSubagentStop().handle(stopInput, invocation); + if (stopResult == null) { + return CompletableFuture.completedFuture(null); + } + return stopResult.thenApply(output -> (Object) output); + } + break; default : LOG.fine("Unhandled hook type: " + hookType); } @@ -2729,7 +2836,7 @@ public void close() { } eventHandlers.clear(); - toolHandlers.clear(); + toolHandlers.set(Map.of()); commandHandlers.clear(); permissionHandler.set(null); userInputHandler.set(null); diff --git a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java index afc30ff6ae..08101c1542 100644 --- a/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/sdk/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -11,6 +11,7 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Function; +import com.fasterxml.jackson.core.type.TypeReference; import com.github.copilot.rpc.CopilotClientMode; import com.github.copilot.rpc.CreateSessionRequest; import com.github.copilot.rpc.ProviderConfig; @@ -23,6 +24,10 @@ import com.github.copilot.rpc.SectionOverrideAction; import com.github.copilot.rpc.SessionConfig; import com.github.copilot.rpc.SystemMessageConfig; +import com.github.copilot.rpc.ToolDefinition; +import com.github.copilot.rpc.ToolDefer; +import com.github.copilot.generated.rpc.ProtocolExternalToolDefer; +import com.github.copilot.generated.rpc.ProtocolExternalToolDefinition; /** * Builds JSON-RPC request objects from session configuration. @@ -89,6 +94,39 @@ static ExtractedTransforms extractTransformCallbacks(SystemMessageConfig systemM return new ExtractedTransforms(wireConfig, callbacks); } + static List toProtocolExternalTools(List tools) { + if (tools == null) { + return null; + } + return tools.stream().map(SessionRequestBuilder::toProtocolExternalTool).toList(); + } + + @SuppressWarnings("unchecked") + private static ProtocolExternalToolDefinition toProtocolExternalTool(ToolDefinition tool) { + Map parameters = null; + if (tool.parameters() instanceof Map map) { + parameters = (Map) map; + } else if (tool.parameters() != null) { + parameters = JsonRpcClient.getObjectMapper().convertValue(tool.parameters(), + new TypeReference>() { + }); + } + return new ProtocolExternalToolDefinition(tool.name(), tool.description() != null ? tool.description() : "", + null, parameters, tool.overridesBuiltInTool(), tool.skipPermission(), toProtocolDefer(tool.defer()), + tool.isTerminal(), tool.metadata()); + } + + private static ProtocolExternalToolDefer toProtocolDefer(ToolDefer defer) { + if (defer == null || defer == ToolDefer.NONE) { + return null; + } + return switch (defer) { + case AUTO -> ProtocolExternalToolDefer.AUTO; + case NEVER -> ProtocolExternalToolDefer.NEVER; + case NONE -> null; + }; + } + /** * Builds a CreateSessionRequest from the given configuration. * @@ -257,6 +295,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setModel(config.getModel()); config.getContinuePendingWork().ifPresent(request::setContinuePendingWork); + config.getAllowTranscriptRecovery().ifPresent(request::setAllowTranscriptRecovery); request.setClientName(config.getClientName()); request.setReasoningEffort(config.getReasoningEffort()); request.setReasoningSummary(config.getReasoningSummary()); diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/NamedProviderConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/NamedProviderConfig.java index e3b090019b..5e2e69cde7 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/NamedProviderConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/NamedProviderConfig.java @@ -51,6 +51,9 @@ public class NamedProviderConfig { @JsonProperty("wireApi") private String wireApi; + @JsonProperty("modelProvider") + private String modelProvider; + @JsonProperty("baseUrl") private String baseUrl; @@ -144,6 +147,30 @@ public NamedProviderConfig setWireApi(String wireApi) { return this; } + /** + * Gets the product serving this provider's models. + * + * @return the model provider, such as "ollama" or "lm_studio" + */ + public String getModelProvider() { + return modelProvider; + } + + /** + * Sets the product serving this provider's models, reported in telemetry as + * {@code model_provider}. Allowed values are "openai", "anthropic", + * "azure_openai", "ollama", "lm_studio", "foundry_local", and "llama_cpp"; only + * affects telemetry. + * + * @param modelProvider + * the model provider + * @return this config for method chaining + */ + public NamedProviderConfig setModelProvider(String modelProvider) { + this.modelProvider = modelProvider; + return this; + } + /** * Gets the base URL for the API. * diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ProviderConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/ProviderConfig.java index 3d6faba34d..db04746c38 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ProviderConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ProviderConfig.java @@ -47,6 +47,9 @@ public class ProviderConfig { @JsonProperty("transport") private String transport; + @JsonProperty("modelProvider") + private String modelProvider; + @JsonProperty("baseUrl") private String baseUrl; @@ -153,6 +156,30 @@ public ProviderConfig setTransport(String transport) { return this; } + /** + * Gets the product serving the model. + * + * @return the model provider, such as "ollama" or "lm_studio" + */ + public String getModelProvider() { + return modelProvider; + } + + /** + * Sets the product serving the model, reported in telemetry as + * {@code model_provider}. Allowed values are "openai", "anthropic", + * "azure_openai", "ollama", "lm_studio", "foundry_local", and "llama_cpp"; only + * affects telemetry. + * + * @param modelProvider + * the model provider + * @return this config for method chaining + */ + public ProviderConfig setModelProvider(String modelProvider) { + this.modelProvider = modelProvider; + return this; + } + /** * Gets the base URL for the API. * diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index 35d55e71a9..4bc7de4803 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -58,6 +58,7 @@ public class ResumeSessionConfig { private SessionLimitsConfig sessionLimits; private Boolean enableExperimentalMode; private Boolean continuePendingWork; + private Boolean allowTranscriptRecovery; private Boolean skipCustomInstructions; private Boolean customAgentsLocalOnly; private Boolean coauthorEnabled; @@ -174,6 +175,40 @@ public ResumeSessionConfig clearContinuePendingWork() { return this; } + /** + * Gets the explicit transcript repair choice on resume. + * + * @return the choice, or empty for the runtime default (true in all modes) + */ + @JsonIgnore + public Optional getAllowTranscriptRecovery() { + return Optional.ofNullable(allowTranscriptRecovery); + } + + /** + * Sets 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 the returned session's report. + * + * @param allowTranscriptRecovery + * whether repair is allowed + * @return this config + */ + public ResumeSessionConfig setAllowTranscriptRecovery(boolean allowTranscriptRecovery) { + this.allowTranscriptRecovery = allowTranscriptRecovery; + return this; + } + + /** + * Clears the transcript repair choice so the runtime default (true) applies. + * + * @return this config for method chaining + */ + public ResumeSessionConfig clearAllowTranscriptRecovery() { + this.allowTranscriptRecovery = null; + return this; + } + /** * Gets the client name used to identify the application using the SDK. * @@ -2187,6 +2222,7 @@ public ResumeSessionConfig clone() { copy.sessionLimits = this.sessionLimits; copy.enableExperimentalMode = this.enableExperimentalMode; copy.continuePendingWork = this.continuePendingWork; + copy.allowTranscriptRecovery = this.allowTranscriptRecovery; copy.reasoningEffort = this.reasoningEffort; copy.reasoningSummary = this.reasoningSummary; copy.contextTier = this.contextTier; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index 9bd25241d8..d25893ff19 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -42,6 +42,9 @@ public final class ResumeSessionRequest { @JsonProperty("continuePendingWork") private Boolean continuePendingWork; + @JsonProperty("allowTranscriptRecovery") + private Boolean allowTranscriptRecovery; + @JsonProperty("reasoningEffort") private String reasoningEffort; @@ -316,6 +319,19 @@ public void clearContinuePendingWork() { continuePendingWork = null; } + /** @return whether transcript repair was requested, or null if omitted */ + public Boolean getAllowTranscriptRecovery() { + return allowTranscriptRecovery; + } + + /** + * @param allowTranscriptRecovery + * whether transcript repair is allowed + */ + public void setAllowTranscriptRecovery(boolean allowTranscriptRecovery) { + this.allowTranscriptRecovery = allowTranscriptRecovery; + } + /** Gets the reasoning effort. @return the reasoning effort level */ public String getReasoningEffort() { return reasoningEffort; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionResponse.java b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionResponse.java index 0f74eb5acb..1b67bd46c1 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionResponse.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ResumeSessionResponse.java @@ -20,11 +20,30 @@ * @param openCanvases * the canvas instances open for the session, or {@code null} (since * 1.0.1) + * @param transcriptRecovery + * repair details, or {@code null} if no repair was reported * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) public record ResumeSessionResponse(@JsonProperty("sessionId") String sessionId, @JsonProperty("workspacePath") String workspacePath, @JsonProperty("capabilities") SessionCapabilities capabilities, - @JsonProperty("openCanvases") List openCanvases) { + @JsonProperty("openCanvases") List openCanvases, + @JsonProperty("transcriptRecovery") TranscriptRecoveryReport transcriptRecovery) { + /** + * Constructs a response without a transcript recovery report. + * + * @param sessionId + * the session ID + * @param workspacePath + * the workspace path, or {@code null} + * @param capabilities + * the host capabilities, or {@code null} + * @param openCanvases + * the open canvas instances, or {@code null} + */ + public ResumeSessionResponse(String sessionId, String workspacePath, SessionCapabilities capabilities, + List openCanvases) { + this(sessionId, workspacePath, capabilities, openCanvases, null); + } } diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SessionHooks.java b/java/sdk/src/main/java/com/github/copilot/rpc/SessionHooks.java index e476f888ed..30febc54ce 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/SessionHooks.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SessionHooks.java @@ -46,6 +46,8 @@ public class SessionHooks { private SessionStartHandler onSessionStart; private SessionEndHandler onSessionEnd; private AgentStopHandler onAgentStop; + private SubagentStartHandler onSubagentStart; + private SubagentStopHandler onSubagentStop; /** * Gets the pre-tool-use handler. @@ -254,6 +256,48 @@ public SessionHooks setOnAgentStop(AgentStopHandler onAgentStop) { return this; } + /** + * Gets the handler called before a subagent's first turn. + * + * @return the handler, or {@code null} if not set + */ + public SubagentStartHandler getOnSubagentStart() { + return onSubagentStart; + } + + /** + * Sets the handler called before a subagent's first turn. + * + * @param onSubagentStart + * the handler + * @return this instance for method chaining + */ + public SessionHooks setOnSubagentStart(SubagentStartHandler onSubagentStart) { + this.onSubagentStart = onSubagentStart; + return this; + } + + /** + * Gets the handler called after a subagent's turn. + * + * @return the handler, or {@code null} if not set + */ + public SubagentStopHandler getOnSubagentStop() { + return onSubagentStop; + } + + /** + * Sets the handler called after a subagent's turn. + * + * @param onSubagentStop + * the handler + * @return this instance for method chaining + */ + public SessionHooks setOnSubagentStop(SubagentStopHandler onSubagentStop) { + this.onSubagentStop = onSubagentStop; + return this; + } + /** * Returns whether any hooks are registered. * @@ -262,6 +306,6 @@ public SessionHooks setOnAgentStop(AgentStopHandler onAgentStop) { public boolean hasHooks() { return onPreToolUse != null || onPreMcpToolCall != null || onPostToolUse != null || onPostToolUseFailure != null || onUserPromptSubmitted != null || onUserPromptTransformed != null || onSessionStart != null - || onSessionEnd != null || onAgentStop != null; + || onSessionEnd != null || onAgentStop != null || onSubagentStart != null || onSubagentStop != null; } } diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHandler.java b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHandler.java new file mode 100644 index 0000000000..273da17aac --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHandler.java @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import java.util.concurrent.CompletableFuture; + +/** + * Handler invoked before a subagent's first turn. + */ +@FunctionalInterface +public interface SubagentStartHandler { + + /** + * Handles a subagent-start hook invocation. + * + * @param input + * the hook input + * @param invocation + * context about the invocation + * @return context to prepend to the subagent's prompt, or {@code null} + */ + CompletableFuture handle(SubagentStartHookInput input, HookInvocation invocation); +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHookInput.java b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHookInput.java new file mode 100644 index 0000000000..0ab4d426b4 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHookInput.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Input received before a subagent's first turn. + * + * @param sessionId + * the parent session ID + * @param timestamp + * Unix timestamp in milliseconds + * @param cwd + * the parent session's working directory + * @param transcriptPath + * the parent session transcript path + * @param agentName + * the subagent definition name + * @param agentDisplayName + * the optional subagent display name + * @param agentDescription + * the optional subagent description + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record SubagentStartHookInput(@JsonProperty("sessionId") String sessionId, + @JsonProperty("timestamp") long timestamp, @JsonProperty("cwd") String cwd, + @JsonProperty("transcriptPath") String transcriptPath, @JsonProperty("agentName") String agentName, + @JsonProperty("agentDisplayName") String agentDisplayName, + @JsonProperty("agentDescription") String agentDescription) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHookOutput.java b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHookOutput.java new file mode 100644 index 0000000000..5a19ad594f --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStartHookOutput.java @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Output for a subagent-start hook. + * + * @param additionalContext + * context prepended to the subagent's initial prompt, or + * {@code null} + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record SubagentStartHookOutput(@JsonProperty("additionalContext") String additionalContext) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHandler.java b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHandler.java new file mode 100644 index 0000000000..c4c00fd622 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHandler.java @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import java.util.concurrent.CompletableFuture; + +/** + * Handler invoked after a subagent's turn. + */ +@FunctionalInterface +public interface SubagentStopHandler { + + /** + * Handles a subagent-stop hook invocation. + * + * @param input + * the hook input + * @param invocation + * context about the invocation + * @return a block decision or replacement response, or {@code null} to allow + * the stop + */ + CompletableFuture handle(SubagentStopHookInput input, HookInvocation invocation); +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHookInput.java b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHookInput.java new file mode 100644 index 0000000000..299238e034 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHookInput.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Input received after a subagent's turn. + * + * @param sessionId + * the parent session ID + * @param timestamp + * Unix timestamp in milliseconds + * @param cwd + * the parent session's working directory + * @param transcriptPath + * the parent session transcript path + * @param agentName + * the subagent definition name + * @param agentType + * the subagent type + * @param agentId + * the subagent ID, if available + * @param agentDisplayName + * the optional subagent display name + * @param agentDescription + * the optional subagent description + * @param stopReason + * the reason the subagent stopped + * @param response + * the subagent's last assistant response + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record SubagentStopHookInput(@JsonProperty("sessionId") String sessionId, + @JsonProperty("timestamp") long timestamp, @JsonProperty("cwd") String cwd, + @JsonProperty("transcriptPath") String transcriptPath, @JsonProperty("agentName") String agentName, + @JsonProperty("agentType") String agentType, @JsonProperty("agentId") String agentId, + @JsonProperty("agentDisplayName") String agentDisplayName, + @JsonProperty("agentDescription") String agentDescription, @JsonProperty("stopReason") String stopReason, + @JsonProperty("response") String response) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHookOutput.java b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHookOutput.java new file mode 100644 index 0000000000..364d7ea88e --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SubagentStopHookOutput.java @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Output for a subagent-stop hook. + * + * @param decision + * {@code "block"} to run another subagent turn, or {@code "allow"} + * (or {@code null}) to allow the stop; other values fail the + * subagent + * @param reason + * the nonempty follow-up instruction required when blocking; invalid + * without {@code decision = "block"} + * @param modifiedResponse + * replacement final response when the stop is allowed + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record SubagentStopHookOutput(@JsonProperty("decision") String decision, @JsonProperty("reason") String reason, + @JsonProperty("modifiedResponse") String modifiedResponse) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SystemMessageSections.java b/java/sdk/src/main/java/com/github/copilot/rpc/SystemMessageSections.java index ca410e497e..d575622f00 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/SystemMessageSections.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SystemMessageSections.java @@ -61,9 +61,9 @@ public abstract sealed class SystemMessageSections permits SystemPromptSections public static final String CUSTOM_INSTRUCTIONS = "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. * * @since 1.3.0 */ @@ -71,7 +71,8 @@ public abstract sealed class SystemMessageSections permits SystemPromptSections /** * End-of-prompt instructions: parallel tool calling, persistence, task - * completion. + * completion, and configured subagent-model guidance when the task tool is + * available. */ public static final String LAST_INSTRUCTIONS = "last_instructions"; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/TranscriptRecoveryReport.java b/java/sdk/src/main/java/com/github/copilot/rpc/TranscriptRecoveryReport.java new file mode 100644 index 0000000000..bf49bc596e --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/TranscriptRecoveryReport.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +/** + * Details of transcript repair reported by session.resume. Invalid line numbers + * include discarded torn-tail data when applicable. + * + * @param plannedBackupPath + * path planned for the original transcript backup + * @param invalidLineNumbers + * affected transcript line numbers + * @param sessionStartMoved + * whether the session start event was relocated + */ +public record TranscriptRecoveryReport(@JsonProperty("plannedBackupPath") String plannedBackupPath, + @JsonProperty("invalidLineNumbers") List invalidLineNumbers, + @JsonProperty("sessionStartMoved") boolean sessionStartMoved) { +} diff --git a/java/sdk/src/test/java/com/github/copilot/AhpHostTest.java b/java/sdk/src/test/java/com/github/copilot/AhpHostTest.java index 1b7f3777d4..2ffaf50442 100644 --- a/java/sdk/src/test/java/com/github/copilot/AhpHostTest.java +++ b/java/sdk/src/test/java/com/github/copilot/AhpHostTest.java @@ -7,9 +7,8 @@ import static org.junit.jupiter.api.Assertions.*; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.copilot.generated.rpc.HostCreateSessionParams; -import com.github.copilot.generated.rpc.HostSessionCreateCallback; +import com.github.copilot.generated.rpc.HostGitHubEnvironmentOptions; +import com.github.copilot.generated.rpc.HostLocalServerOptions; import com.github.copilot.generated.rpc.ServerHostApi; import com.github.copilot.generated.rpc.SessionLimitsConfig; import com.github.copilot.rpc.CopilotClientOptions; @@ -36,10 +35,94 @@ @AllowCopilotExperimental class AhpHostTest { + private static AhpHostOptions localOptions() { + return new AhpHostOptions().setLocalServer(new HostLocalServerOptions(null, null, null, null)); + } + private static T await(CompletableFuture future) throws Exception { return future.get(10, TimeUnit.SECONDS); } + @Test + void requiresExplicitTransportBeforeConnecting() throws Exception { + try (var server = new FakeRuntime(); var client = server.client()) { + for (AhpHostOptions options : Arrays.asList(null, new AhpHostOptions())) { + var error = assertThrows(Exception.class, () -> await(client.startAhpHost(options))); + assertInstanceOf(IllegalArgumentException.class, error.getCause()); + } + assertFalse(server.connected.isDone()); + } + } + + @Test + void forwardsEnvironmentFieldsForRuntimeValidation() throws Exception { + for (String[] fields : new String[][]{{"", "compute"}, {" ", "compute"}, {"host", ""}, {"host", " "}}) { + try (var server = new FakeRuntime(); + var client = server.client(); + var host = await(client.startAhpHost(new AhpHostOptions() + .setGithubEnvironment(new HostGitHubEnvironmentOptions(fields[0], fields[1]))))) { + var environment = await(server.startRequest).path("githubEnvironment"); + assertEquals(fields[0], environment.path("name").asText()); + assertEquals(fields[1], environment.path("computeId").asText()); + } + } + } + + @ParameterizedTest + @CsvSource({"true,false", "false,true", "true,true"}) + void forwardsExplicitTransportsAndOptionalReadiness(boolean local, boolean github) throws Exception { + try (var server = new FakeRuntime(); var client = server.client()) { + var options = new AhpHostOptions(); + if (local) { + options.setLocalServer(new HostLocalServerOptions("127.0.0.1", 0L, "test-token", true)); + } + if (github) { + options.setGithubEnvironment(new HostGitHubEnvironmentOptions("test host", "compute")); + } + try (var host = await(client.startAhpHost(options))) { + var request = await(server.startRequest); + assertEquals(local, request.hasNonNull("localServer")); + assertEquals(github, request.hasNonNull("githubEnvironment")); + for (String oldField : List.of("hostname", "port", "token", "requireConnectionToken")) { + assertFalse(request.has(oldField)); + } + if (local) { + assertEquals("test-token", request.path("localServer").path("token").asText()); + assertEquals(0, request.path("localServer").path("port").asInt()); + } + if (github) { + assertEquals("test host", request.path("githubEnvironment").path("name").asText()); + assertEquals("compute", request.path("githubEnvironment").path("computeId").asText()); + } + assertEquals(local ? "ws://127.0.0.1:12345" : null, host.getUrl()); + assertEquals(local ? "test-token" : null, host.getToken()); + assertEquals(github ? "environment-123" : null, host.getEnvironmentId()); + assertNull(host.getPid()); + } + } + } + + @Test + void snapshotsOptionsBeforeConnecting() throws Exception { + try (var server = new FakeRuntime(); var client = server.client()) { + server.finishConnect = new CompletableFuture<>(); + var options = localOptions().setGithubEnvironment(new HostGitHubEnvironmentOptions("original", "compute")); + var pending = client.startAhpHost(options); + await(server.connectEntered); + try { + options.setLocalServer(null).setGithubEnvironment(new HostGitHubEnvironmentOptions("", "")); + } finally { + server.finishConnect.complete(null); + } + try (var host = await(pending)) { + var request = await(server.startRequest); + assertTrue(request.hasNonNull("localServer")); + assertEquals("original", request.path("githubEnvironment").path("name").asText()); + assertEquals("compute", request.path("githubEnvironment").path("computeId").asText()); + } + } + } + @Test void publicHostApiExposesOnlyOwnerOperations() { var methods = Arrays.stream(ServerHostApi.class.getMethods()) @@ -48,32 +131,11 @@ void publicHostApiExposesOnlyOwnerOperations() { assertEquals(Set.of("start", "dispose", "publishSession"), methods); } - @Test - void generatedHandoffMapsRoundTripScalarAndStructuredSettings() throws Exception { - var mapper = new ObjectMapper(); - Map config = new HashMap<>(); - config.put("sessionId", "requested"); - config.put("workingDirectory", "/workspace"); - config.put("streaming", true); - config.put("threshold", 42); - config.put("additionalDirectories", List.of("/other")); - config.put("featureFlags", Map.of("flag", true)); - config.put("optional", null); - var request = new HostCreateSessionParams("handoff", false, config); - assertEquals(config, - mapper.readValue(mapper.writeValueAsBytes(request), HostCreateSessionParams.class).config()); - var callback = mapper.readValue( - mapper.writeValueAsBytes( - Map.of("hostId", "host", "handoffId", "handoff", "resume", false, "config", config)), - HostSessionCreateCallback.class); - assertEquals(config, callback.config()); - } - @Test void cancelledStartupDisposesAfterStartCompletes() throws Exception { try (var server = new FakeRuntime(); var client = server.client()) { server.finishStart = new CompletableFuture<>(); - var pending = client.startAhpHost(new AhpHostOptions()); + var pending = client.startAhpHost(localOptions()); String hostId = await(server.startEntered); assertTrue(pending.cancel(false)); assertEquals(0, server.disposals.get()); @@ -90,7 +152,7 @@ void preservesFactoryConfigurationAndReleasesExactObject(boolean resume, boolean try (var server = new FakeRuntime(); var client = server.client()) { var created = new CompletableFuture(); var released = new CompletableFuture(); - var options = new AhpHostOptions().setCreateSession(request -> client + var options = localOptions().setCreateSession(request -> client .createSession(request.config().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) .thenApply(session -> { created.complete(session); @@ -132,7 +194,7 @@ void preservesFactoryConfigurationAndReleasesExactObject(boolean resume, boolean "suppressResumeEvent,false", "suppressResumeEvent,true"}) void rejectsChangedBooleanSettings(String setting, boolean expected) throws Exception { try (var server = new FakeRuntime(); var client = server.client()) { - var host = await(client.startAhpHost(new AhpHostOptions().setResumeSession(request -> { + var host = await(client.startAhpHost(localOptions().setResumeSession(request -> { switch (setting) { case "streaming" -> request.config().setStreaming(!expected); case "enableMcpApps" -> request.config().setEnableMcpApps(!expected); @@ -155,7 +217,7 @@ void comparesNumericSettingsByValue(boolean resume) throws Exception { for (Number credits : List.of(10, 3000000000L, 10.5)) { for (boolean changed : List.of(false, true)) { try (var server = new FakeRuntime(); var client = server.client()) { - var host = await(client.startAhpHost(new AhpHostOptions().setCreateSession(request -> { + var host = await(client.startAhpHost(localOptions().setCreateSession(request -> { if (changed) { request.config().setSessionLimits(new SessionLimitsConfig(credits.doubleValue() + 1)); } @@ -190,7 +252,7 @@ void cancellationUnblocksRpcAndReleasesLateResultAfterOwnerStops() throws Except var released = new CompletableFuture(); var original = await(client.createSession(new SessionConfig().setSessionId("requested") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL))); - var host = await(client.startAhpHost(new AhpHostOptions().setCreateSession(request -> { + var host = await(client.startAhpHost(localOptions().setCreateSession(request -> { entered.complete(request.cancellation()); return late; }).setOnSessionReleased(session -> { @@ -217,7 +279,7 @@ void retainedResumeDoesNotReconfigureOriginal() throws Exception { var original = await(client.createSession(new SessionConfig().setSessionId("retained") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL))); var host = await(client.startAhpHost( - new AhpHostOptions().setResumeSession(request -> CompletableFuture.completedFuture(original)))); + localOptions().setResumeSession(request -> CompletableFuture.completedFuture(original)))); assertEquals("retained", await(server.materialize(host, "resume", true, Map.of("sessionId", "retained", "workingDirectory", "/new/default"))).get("sessionId") @@ -237,7 +299,7 @@ void rejectsForeignObjectsAndChangedHostSettings() throws Exception { .setOnPermissionRequest(PermissionHandler.APPROVE_ALL))); var released = new CompletableFuture(); var host = await(client.startAhpHost( - new AhpHostOptions().setCreateSession(request -> CompletableFuture.completedFuture(wrongOwner)) + localOptions().setCreateSession(request -> CompletableFuture.completedFuture(wrongOwner)) .setOnSessionReleased(session -> { released.complete(session); return CompletableFuture.completedFuture(null); @@ -247,9 +309,8 @@ void rejectsForeignObjectsAndChangedHostSettings() throws Exception { assertTrue(error.getMessage().contains("owning client")); assertSame(wrongOwner, await(released)); await(host.dispose()); - var changed = await( - client.startAhpHost(new AhpHostOptions().setCreateSession(request -> client.createSession(request - .config().setModel("changed").setOnPermissionRequest(PermissionHandler.APPROVE_ALL))))); + var changed = await(client.startAhpHost(localOptions().setCreateSession(request -> client.createSession( + request.config().setModel("changed").setOnPermissionRequest(PermissionHandler.APPROVE_ALL))))); error = assertThrows(Exception.class, () -> await(server.materialize(changed, "changed", false, Map.of("sessionId", "requested", "model", "original")))); assertTrue(error.getMessage().contains("config.model")); @@ -262,7 +323,7 @@ void publishesDisposesRepeatedlyAndRetainsOriginalTransport() throws Exception { try (var server = new FakeRuntime(); var client = server.client()) { var exits = new AtomicInteger(); var exited = new CompletableFuture(); - var host = await(client.startAhpHost(new AhpHostOptions().setOnExit(info -> { + var host = await(client.startAhpHost(localOptions().setOnExit(info -> { exits.incrementAndGet(); exited.complete(null); return CompletableFuture.completedFuture(null); @@ -285,7 +346,10 @@ private static final class FakeRuntime implements AutoCloseable { final CopyOnWriteArrayList sessionRequests = new CopyOnWriteArrayList<>(); final AtomicInteger disposals = new AtomicInteger(); final CompletableFuture startEntered = new CompletableFuture<>(); + final CompletableFuture startRequest = new CompletableFuture<>(); final CompletableFuture disposedHost = new CompletableFuture<>(); + final CompletableFuture connectEntered = new CompletableFuture<>(); + volatile CompletableFuture finishConnect = CompletableFuture.completedFuture(null); volatile CompletableFuture finishStart = CompletableFuture.completedFuture(null); final Thread accept; @@ -293,8 +357,11 @@ private static final class FakeRuntime implements AutoCloseable { accept = new Thread(() -> { try { var rpc = JsonRpcClient.fromSocket(listener.accept(), transport -> { - transport.registerMethodHandler("connect", (id, params) -> respond(transport, id, - Map.of("ok", true, "protocolVersion", 3, "version", "test"))); + transport.registerMethodHandler("connect", (id, params) -> { + connectEntered.complete(null); + finishConnect.thenRun(() -> respond(transport, id, + Map.of("ok", true, "protocolVersion", 3, "version", "test"))); + }); for (String method : List.of("session.create", "session.resume")) { transport.registerMethodHandler(method, (id, params) -> { sessionRequests.add(params); @@ -307,9 +374,17 @@ private static final class FakeRuntime implements AutoCloseable { } transport.registerMethodHandler("host.start", (id, params) -> { startEntered.complete(params.get("hostId").asText()); - finishStart.thenRun( - () -> respond(transport, id, Map.of("hostId", params.get("hostId").asText(), "url", - "ws://127.0.0.1:12345", "token", "test-token"))); + startRequest.complete(params); + var result = new HashMap(); + result.put("hostId", params.get("hostId").asText()); + if (params.hasNonNull("localServer")) { + result.put("url", "ws://127.0.0.1:12345"); + result.put("token", "test-token"); + } + if (params.hasNonNull("githubEnvironment")) { + result.put("environmentId", "environment-123"); + } + finishStart.thenRun(() -> respond(transport, id, result)); }); transport.registerMethodHandler("host.publishSession", (id, params) -> respond(transport, id, diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java b/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java index 574f057467..9055cb19f7 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import com.github.copilot.generated.ExternalToolRequestedEvent; import com.github.copilot.rpc.CopilotClientOptions; @@ -20,6 +21,12 @@ import java.io.OutputStream; import java.lang.reflect.Field; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -30,6 +37,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; @@ -255,6 +263,111 @@ void testShouldReportErrorWithStderrWhenCliFailsToStart() throws Exception { } } + @Test + void testStartupWriteFailureDrainsStderrBeforeKillingChild(@TempDir Path tempDir) throws Exception { + var script = tempDir.resolve("startup-error.js"); + Files.writeString(script, """ + const fs = require('node:fs'); + fs.watch(__dirname, () => { + if (fs.existsSync(__dirname + '/release-stderr')) { + fs.writeSync(2, 'error: --nonexistent-flag-for-testing\\n'); + process.exit(1); + } + }); + fs.closeSync(0); + process.stdout.write('started\\n'); + """); + var options = new CopilotClientOptions().setCliPath(script.toString()).setUseStdio(true); + var manager = spy(new CliServerManager(options)); + var child = new AtomicReference(); + doAnswer(invocation -> { + Process process = invocation.getArgument(0); + child.set(process); + var reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); + assertEquals("started", reader.readLine()); + return invocation.callRealMethod(); + }).when(manager).connectToServer(any(Process.class), isNull(), isNull()); + doAnswer(invocation -> { + // Release stderr only when startup drains it, while the child is still alive. + Files.createFile(tempDir.resolve("release-stderr")); + return invocation.callRealMethod(); + }).when(manager).awaitStderrReader(); + + try (var client = new CopilotClient(options)) { + Field managerField = CopilotClient.class.getDeclaredField("serverManager"); + managerField.setAccessible(true); + managerField.set(client, manager); + + var error = assertThrows(ExecutionException.class, () -> client.start().get(10, TimeUnit.SECONDS)); + var cause = error.getCause(); + assertTrue(cause.getMessage().contains("--nonexistent-flag-for-testing"), + "Startup lost the child's stderr: " + cause); + assertInstanceOf(IOException.class, cause); + Throwable original = cause; + while (original.getCause() != null) { + original = original.getCause(); + } + assertInstanceOf(IOException.class, original); + assertNotSame(cause, original, "The startup error must retain its original transport failure."); + assertFalse(child.get().isAlive(), "Failed startup retained its child process."); + } finally { + if (child.get() != null && child.get().isAlive()) { + child.get().destroyForcibly(); + assertTrue(child.get().waitFor(10, TimeUnit.SECONDS), "Could not reap the controlled child."); + } + } + } + + @Test + void testProtocolMismatchTerminatesLiveChildBeforeDrainingStderr(@TempDir Path tempDir) throws Exception { + var script = tempDir.resolve("incompatible-server.js"); + Files.writeString(script, """ + let input = Buffer.alloc(0); + process.stdin.on('data', chunk => { + input = Buffer.concat([input, chunk]); + const headerEnd = input.indexOf('\\r\\n\\r\\n'); + if (headerEnd < 0) return; + const length = Number(/Content-Length:\\s*(\\d+)/i.exec(input.toString())[1]); + if (input.length < headerEnd + 4 + length) return; + const request = JSON.parse(input.subarray(headerEnd + 4, headerEnd + 4 + length)); + const response = JSON.stringify({ + jsonrpc: '2.0', id: request.id, result: { protocolVersion: 1 } + }); + process.stdout.write(`Content-Length: ${Buffer.byteLength(response)}\\r\\n\\r\\n${response}`); + input = input.subarray(headerEnd + 4 + length); + }); + setInterval(() => {}, 1000); + """); + var options = new CopilotClientOptions().setCliPath(script.toString()).setUseStdio(true); + var manager = spy(new CliServerManager(options)); + var child = new AtomicReference(); + doAnswer(invocation -> { + child.set(invocation.getArgument(0)); + return invocation.callRealMethod(); + }).when(manager).connectToServer(any(Process.class), isNull(), isNull()); + doAnswer(invocation -> { + assertFalse(child.get().isAlive(), "A rejected live server must exit before its stderr reader is joined."); + return invocation.callRealMethod(); + }).when(manager).awaitStderrReader(); + + try (var client = new CopilotClient(options)) { + Field managerField = CopilotClient.class.getDeclaredField("serverManager"); + managerField.setAccessible(true); + managerField.set(client, manager); + + var error = assertThrows(ExecutionException.class, () -> client.start().get(10, TimeUnit.SECONDS)); + assertTrue(error.getCause().getMessage().contains("SDK protocol version mismatch"), + "Startup must report the incompatible protocol: " + error.getCause()); + assertFalse(child.get().isAlive(), "Failed startup retained its child process."); + verify(manager).awaitStderrReader(); + } finally { + if (child.get() != null && child.get().isAlive()) { + child.get().destroyForcibly(); + assertTrue(child.get().waitFor(10, TimeUnit.SECONDS), "Could not reap the incompatible server."); + } + } + } + @Test void testStartAndConnectUsingTcp() throws Exception { assertNotNull(cliPath, "Copilot CLI not found in PATH or COPILOT_CLI_PATH"); diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotSessionTest.java b/java/sdk/src/test/java/com/github/copilot/CopilotSessionTest.java index 5bf5be8820..94ccda7243 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotSessionTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotSessionTest.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -146,7 +147,7 @@ void testShouldReceiveSessionEvents() throws Exception { CopilotSession session = client .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); - List receivedEvents = new ArrayList<>(); + List receivedEvents = new CopyOnWriteArrayList<>(); CompletableFuture idleReceived = new CompletableFuture<>(); session.on(evt -> { diff --git a/java/sdk/src/test/java/com/github/copilot/ErrorHandlingTest.java b/java/sdk/src/test/java/com/github/copilot/ErrorHandlingTest.java index e091eea71a..e8283c2948 100644 --- a/java/sdk/src/test/java/com/github/copilot/ErrorHandlingTest.java +++ b/java/sdk/src/test/java/com/github/copilot/ErrorHandlingTest.java @@ -10,6 +10,7 @@ import java.util.logging.Logger; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterAll; @@ -61,7 +62,7 @@ void testHandlesToolCallingErrors_toolErrorDoesNotCrashSession() throws Exceptio LOG.info("Running test: testHandlesToolCallingErrors_toolErrorDoesNotCrashSession"); ctx.configureForTest("tools", "handles_tool_calling_errors"); - var allEvents = new ArrayList(); + var allEvents = new CopyOnWriteArrayList(); var idleReceived = new CompletableFuture(); ToolDefinition errorTool = ToolDefinition.create("get_user_location", "Gets the user's location", diff --git a/java/sdk/src/test/java/com/github/copilot/MultiProviderConfigTest.java b/java/sdk/src/test/java/com/github/copilot/MultiProviderConfigTest.java index 171e525cf3..fc6dba15f4 100644 --- a/java/sdk/src/test/java/com/github/copilot/MultiProviderConfigTest.java +++ b/java/sdk/src/test/java/com/github/copilot/MultiProviderConfigTest.java @@ -39,6 +39,7 @@ void testNamedProviderConfigDefaultsAreNull() { assertNull(provider.getName()); assertNull(provider.getType()); assertNull(provider.getWireApi()); + assertNull(provider.getModelProvider()); assertNull(provider.getBaseUrl()); assertNull(provider.getApiKey()); assertNull(provider.getBearerToken()); @@ -51,8 +52,8 @@ void testNamedProviderConfigFluentSettersReturnSameInstance() { var provider = new NamedProviderConfig(); NamedProviderConfig result = provider.setName("my-openai").setType("openai").setWireApi("responses") - .setBaseUrl("https://api.openai.com/v1").setApiKey("sk-test").setBearerToken("bearer") - .setAzure(new AzureOptions()).setHeaders(Map.of("X-Custom", "v")); + .setModelProvider("ollama").setBaseUrl("https://api.openai.com/v1").setApiKey("sk-test") + .setBearerToken("bearer").setAzure(new AzureOptions()).setHeaders(Map.of("X-Custom", "v")); assertEquals(provider, result); } @@ -60,13 +61,14 @@ void testNamedProviderConfigFluentSettersReturnSameInstance() { @Test void testSerializeNamedProviderConfig() throws Exception { var provider = new NamedProviderConfig().setName("my-openai").setType("openai").setWireApi("responses") - .setBaseUrl("https://api.openai.com/v1").setApiKey("sk-test"); + .setModelProvider("ollama").setBaseUrl("https://api.openai.com/v1").setApiKey("sk-test"); JsonNode json = MAPPER.valueToTree(provider); assertEquals("my-openai", json.get("name").asText()); assertEquals("openai", json.get("type").asText()); assertEquals("responses", json.get("wireApi").asText()); + assertEquals("ollama", json.get("modelProvider").asText()); assertEquals("https://api.openai.com/v1", json.get("baseUrl").asText()); assertEquals("sk-test", json.get("apiKey").asText()); // Null fields must be omitted (NON_NULL) diff --git a/java/sdk/src/test/java/com/github/copilot/ProviderConfigTest.java b/java/sdk/src/test/java/com/github/copilot/ProviderConfigTest.java index effb360401..e5836a0c1b 100644 --- a/java/sdk/src/test/java/com/github/copilot/ProviderConfigTest.java +++ b/java/sdk/src/test/java/com/github/copilot/ProviderConfigTest.java @@ -42,6 +42,7 @@ void testDefaultsAreNull() { assertNull(provider.getType()); assertNull(provider.getWireApi()); + assertNull(provider.getModelProvider()); assertNull(provider.getBaseUrl()); assertNull(provider.getApiKey()); assertNull(provider.getBearerToken()); @@ -52,7 +53,7 @@ void testDefaultsAreNull() { void testFluentSettersReturnSameInstance() { var provider = new ProviderConfig(); - ProviderConfig result = provider.setType("openai").setWireApi("completions") + ProviderConfig result = provider.setType("openai").setWireApi("completions").setModelProvider("lm_studio") .setBaseUrl("https://api.openai.com/v1").setApiKey("sk-test-key").setBearerToken("bearer-token") .setAzure(new AzureOptions()); @@ -63,12 +64,13 @@ void testFluentSettersReturnSameInstance() { @Test void testGettersReturnSetValues() { var azure = new AzureOptions().setApiVersion("2024-02-01"); - var provider = new ProviderConfig().setType("azure-openai").setWireApi("chat") + var provider = new ProviderConfig().setType("azure-openai").setWireApi("chat").setModelProvider("azure_openai") .setBaseUrl("https://my-resource.openai.azure.com").setApiKey("my-key").setBearerToken("my-token") .setAzure(azure); assertEquals("azure-openai", provider.getType()); assertEquals("chat", provider.getWireApi()); + assertEquals("azure_openai", provider.getModelProvider()); assertEquals("https://my-resource.openai.azure.com", provider.getBaseUrl()); assertEquals("my-key", provider.getApiKey()); assertEquals("my-token", provider.getBearerToken()); @@ -253,18 +255,20 @@ void testTransportOmittedWhenNull() throws Exception { @Test void testSerializeAllFields() throws Exception { var provider = new ProviderConfig().setType("azure-openai").setWireApi("completions") - .setBaseUrl("https://my-resource.openai.azure.com").setApiKey("my-api-key") - .setBearerToken("my-bearer-token").setAzure(new AzureOptions().setApiVersion("2024-02-01")); + .setModelProvider("azure_openai").setBaseUrl("https://my-resource.openai.azure.com") + .setApiKey("my-api-key").setBearerToken("my-bearer-token") + .setAzure(new AzureOptions().setApiVersion("2024-02-01")); JsonNode json = MAPPER.valueToTree(provider); assertEquals("azure-openai", json.get("type").asText()); assertEquals("completions", json.get("wireApi").asText()); + assertEquals("azure_openai", json.get("modelProvider").asText()); assertEquals("https://my-resource.openai.azure.com", json.get("baseUrl").asText()); assertEquals("my-api-key", json.get("apiKey").asText()); assertEquals("my-bearer-token", json.get("bearerToken").asText()); assertEquals("2024-02-01", json.get("azure").get("apiVersion").asText()); - assertEquals(6, json.size(), "Expected exactly 6 JSON fields"); + assertEquals(7, json.size(), "Expected exactly 7 JSON fields"); } @Test @@ -417,7 +421,8 @@ void testResumeSessionConfigWithoutProviderOmitsField() throws Exception { void testProviderModelIdAndWireModelSerialization() throws Exception { var provider = new ProviderConfig().setBaseUrl("https://example.com/provider") .setHeaders(java.util.Map.of("Authorization", "Bearer provider-token")).setModelId("gpt-4o") - .setWireModel("my-finetune-v3").setMaxPromptTokens(100_000).setMaxOutputTokens(4096); + .setWireModel("my-finetune-v3").setModelProvider("lm_studio").setMaxPromptTokens(100_000) + .setMaxOutputTokens(4096); JsonNode json = MAPPER.valueToTree(provider); @@ -425,6 +430,7 @@ void testProviderModelIdAndWireModelSerialization() throws Exception { assertEquals("Bearer provider-token", json.get("headers").get("Authorization").asText()); assertEquals("gpt-4o", json.get("modelId").asText()); assertEquals("my-finetune-v3", json.get("wireModel").asText()); + assertEquals("lm_studio", json.get("modelProvider").asText()); assertEquals(100_000, json.get("maxPromptTokens").asInt()); assertEquals(4096, json.get("maxOutputTokens").asInt()); @@ -432,6 +438,7 @@ void testProviderModelIdAndWireModelSerialization() throws Exception { ProviderConfig deserialized = MAPPER.readValue(MAPPER.writeValueAsString(provider), ProviderConfig.class); assertEquals("gpt-4o", deserialized.getModelId()); assertEquals("my-finetune-v3", deserialized.getWireModel()); + assertEquals("lm_studio", deserialized.getModelProvider()); assertEquals(100_000, deserialized.getMaxPromptTokens().getAsInt()); assertEquals(4096, deserialized.getMaxOutputTokens().getAsInt()); } @@ -441,6 +448,7 @@ void testProviderModelFieldsDefaultToNull() { var provider = new ProviderConfig(); assertNull(provider.getModelId()); assertNull(provider.getWireModel()); + assertNull(provider.getModelProvider()); assertTrue(provider.getMaxPromptTokens().isEmpty()); assertTrue(provider.getMaxOutputTokens().isEmpty()); } @@ -453,6 +461,7 @@ void testProviderModelFieldsOmittedWhenNull() throws Exception { assertTrue(json.path("modelId").isMissingNode()); assertTrue(json.path("wireModel").isMissingNode()); + assertTrue(json.path("modelProvider").isMissingNode()); assertTrue(json.path("maxPromptTokens").isMissingNode()); assertTrue(json.path("maxOutputTokens").isMissingNode()); } diff --git a/java/sdk/src/test/java/com/github/copilot/ProviderEndpointE2ETest.java b/java/sdk/src/test/java/com/github/copilot/ProviderEndpointE2ETest.java deleted file mode 100644 index 1e302982ef..0000000000 --- a/java/sdk/src/test/java/com/github/copilot/ProviderEndpointE2ETest.java +++ /dev/null @@ -1,155 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -package com.github.copilot; - -import static org.junit.jupiter.api.Assertions.*; - -import java.util.HashMap; -import java.util.Map; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import com.github.copilot.generated.rpc.ProviderEndpointType; -import com.github.copilot.generated.rpc.ProviderEndpointWireApi; -import com.github.copilot.generated.rpc.ProviderSessionToken; -import com.github.copilot.generated.rpc.SessionProviderGetEndpointResult; -import com.github.copilot.rpc.CopilotClientOptions; -import com.github.copilot.rpc.PermissionHandler; -import com.github.copilot.rpc.ProviderConfig; -import com.github.copilot.rpc.SessionConfig; - -/** - * Tests for the {@code session.provider.getEndpoint} RPC, which surfaces the - * resolved provider endpoint and credentials for either a BYOK or CAPI session. - */ -public class ProviderEndpointE2ETest { - - private static E2ETestContext ctx; - - @BeforeAll - static void setup() throws Exception { - ctx = E2ETestContext.create(); - } - - @AfterAll - static void teardown() throws Exception { - if (ctx != null) { - ctx.close(); - } - } - - // session.provider.getEndpoint is gated behind - // COPILOT_ALLOW_GET_PROVIDER_ENDPOINT; - // the harness env passed to the CLI subprocess opts in for these tests. - private CopilotClient createProviderEndpointClient() { - Map env = new HashMap<>(ctx.getEnvironment()); - env.put("COPILOT_ALLOW_GET_PROVIDER_ENDPOINT", "true"); - return ctx.createClient(new CopilotClientOptions().setEnvironment(env)); - } - - @Test - void shouldReturnByokProviderEndpointWhenCustomProviderConfigured() throws Exception { - try (CopilotClient client = createProviderEndpointClient()) { - Map customHeaders = new HashMap<>(); - customHeaders.put("X-Custom-Header", "byok-yes"); - - ProviderConfig provider = new ProviderConfig().setType("openai").setWireApi("completions") - .setBaseUrl("https://api.example.test/v1").setApiKey("byok-secret").setHeaders(customHeaders); - - CopilotSession session = client.createSession( - new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setProvider(provider)) - .get(); - - try { - SessionProviderGetEndpointResult endpoint = session.getRpc().provider.getEndpoint().get(); - - assertEquals(ProviderEndpointType.OPENAI, endpoint.type()); - assertEquals(ProviderEndpointWireApi.COMPLETIONS, endpoint.wireApi()); - assertEquals("https://api.example.test/v1", endpoint.baseUrl()); - assertEquals("byok-secret", endpoint.apiKey()); - assertEquals("byok-yes", endpoint.headers().get("X-Custom-Header")); - // BYOK sessions never issue a CAPI session token. - assertNull(endpoint.sessionToken(), "BYOK session should not have a session token"); - } finally { - try { - session.close(); - } catch (Exception ignored) { - // disconnect may fail since the BYOK provider URL is fake - } - } - } - } - - @Test - void shouldReturnCapiProviderEndpointForOAuthAuthenticatedSession() throws Exception { - ctx.initializeProxy(); - ctx.setCopilotUserByToken("fake-token-for-e2e-tests", "e2e-user", "individual_pro", ctx.getProxyUrl(), - "https://localhost:1/telemetry", "e2e-tracking-id"); - - try (CopilotClient client = createProviderEndpointClient()) { - CopilotSession session = client - .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); - - try { - SessionProviderGetEndpointResult endpoint = session.getRpc().provider.getEndpoint().get(); - - assertNotNull(endpoint.type(), "CAPI endpoint should have a provider type"); - assertTrue( - endpoint.type() == ProviderEndpointType.OPENAI || endpoint.type() == ProviderEndpointType.AZURE - || endpoint.type() == ProviderEndpointType.ANTHROPIC, - "expected type in {openai, azure, anthropic}, got " + endpoint.type()); - // wireApi is omitted for anthropic; otherwise one of the OpenAI shapes. - if (endpoint.type() != ProviderEndpointType.ANTHROPIC) { - assertTrue( - endpoint.wireApi() == ProviderEndpointWireApi.COMPLETIONS - || endpoint.wireApi() == ProviderEndpointWireApi.RESPONSES, - "expected wireApi in {completions, responses}, got " + endpoint.wireApi()); - } - - // CAPI baseUrl is the (proxy) Copilot API URL injected by the harness. - assertTrue(endpoint.baseUrl().startsWith("http://") || endpoint.baseUrl().startsWith("https://"), - "expected http(s) baseUrl, got " + endpoint.baseUrl()); - - // For CAPI OAuth sessions the apiKey is the resolved GitHub bearer. - assertNotNull(endpoint.apiKey(), "CAPI OAuth session must surface apiKey"); - assertFalse(endpoint.apiKey().isEmpty(), "apiKey must be non-empty"); - - Map headers = endpoint.headers(); - String integrationId = headers.get("Copilot-Integration-Id"); - assertNotNull(integrationId, "Copilot-Integration-Id header must be present"); - assertFalse(integrationId.isEmpty(), "Copilot-Integration-Id must be non-empty"); - - String userAgent = headers.get("User-Agent"); - assertNotNull(userAgent, "User-Agent header must be present"); - assertTrue(userAgent.toLowerCase().contains("copilot"), - "expected User-Agent to mention Copilot, got " + userAgent); - - String apiVersion = headers.get("X-GitHub-Api-Version"); - assertNotNull(apiVersion, "X-GitHub-Api-Version header must be present"); - assertFalse(apiVersion.isEmpty(), "X-GitHub-Api-Version must be non-empty"); - - String interactionId = headers.get("X-Interaction-Id"); - assertNotNull(interactionId, "X-Interaction-Id header must be present"); - assertTrue(interactionId.matches(".*[0-9a-f-]{8,}.*"), - "expected X-Interaction-Id to look like a hex/uuid value, got " + interactionId); - - String authorization = headers.get("Authorization"); - assertEquals("Bearer " + endpoint.apiKey(), authorization); - - ProviderSessionToken sessionToken = endpoint.sessionToken(); - if (sessionToken != null) { - assertEquals("Copilot-Session-Token", sessionToken.header()); - assertFalse(sessionToken.token().isEmpty(), "session token must be non-empty"); - // expiresAt is optional; when present it parses as OffsetDateTime so no - // additional validation is needed. - } - } finally { - session.close(); - } - } - } -} diff --git a/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java b/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java deleted file mode 100644 index ca7e52a409..0000000000 --- a/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java +++ /dev/null @@ -1,607 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -package com.github.copilot; - -import static org.junit.jupiter.api.Assertions.*; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.OffsetDateTime; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import com.github.copilot.generated.rpc.AccountQuotaSnapshot; -import com.github.copilot.generated.rpc.AgentsDiscoverParams; -import com.github.copilot.generated.rpc.AgentsGetDiscoveryPathsParams; -import com.github.copilot.generated.rpc.InstructionsDiscoverParams; -import com.github.copilot.generated.rpc.InstructionsGetDiscoveryPathsParams; -import com.github.copilot.generated.rpc.LlmInferenceHttpResponseChunkError; -import com.github.copilot.generated.rpc.LlmInferenceHttpResponseChunkParams; -import com.github.copilot.generated.rpc.LlmInferenceHttpResponseStartParams; -import com.github.copilot.generated.rpc.LocalSessionMetadataValue; -import com.github.copilot.generated.rpc.McpDiscoverParams; -import com.github.copilot.generated.rpc.PingParams; -import com.github.copilot.generated.rpc.SecretsAddFilterValuesParams; -import com.github.copilot.generated.rpc.ServerSkill; -import com.github.copilot.generated.rpc.SessionContext; -import com.github.copilot.generated.rpc.SessionFsSetProviderCapabilities; -import com.github.copilot.generated.rpc.SessionFsSetProviderConventions; -import com.github.copilot.generated.rpc.SessionFsSetProviderParams; -import com.github.copilot.generated.rpc.SessionsBulkDeleteParams; -import com.github.copilot.generated.rpc.SessionsCheckInUseParams; -import com.github.copilot.generated.rpc.SessionsCloseParams; -import com.github.copilot.generated.rpc.SessionsConnectParams; -import com.github.copilot.generated.rpc.SessionsEnrichMetadataParams; -import com.github.copilot.generated.rpc.SessionsFindByPrefixParams; -import com.github.copilot.generated.rpc.SessionsFindByTaskIdParams; -import com.github.copilot.generated.rpc.SessionsGetEventFilePathParams; -import com.github.copilot.generated.rpc.SessionsGetLastForContextParams; -import com.github.copilot.generated.rpc.SessionsGetPersistedRemoteSteerableParams; -import com.github.copilot.generated.rpc.SessionsLoadDeferredRepoHooksParams; -import com.github.copilot.generated.rpc.SessionsPruneOldParams; -import com.github.copilot.generated.rpc.SessionsReleaseLockParams; -import com.github.copilot.generated.rpc.SessionsReloadPluginHooksParams; -import com.github.copilot.generated.rpc.SessionsSaveParams; -import com.github.copilot.generated.rpc.SessionsSetAdditionalPluginsParams; -import com.github.copilot.generated.rpc.SkillsConfigSetDisabledSkillsParams; -import com.github.copilot.generated.rpc.SkillsDiscoverParams; -import com.github.copilot.generated.rpc.SkillsGetDiscoveryPathsParams; -import com.github.copilot.generated.rpc.ToolsListParams; -import com.github.copilot.rpc.CopilotClientOptions; -import com.github.copilot.rpc.InfiniteSessionConfig; -import com.github.copilot.rpc.PermissionHandler; -import com.github.copilot.rpc.SessionConfig; - -class RpcServerE2ETest { - - private static final long TIMEOUT_SECONDS = 30; - private static final long SESSION_PERSISTENCE_TIMEOUT_MILLIS = 30_000; - private static E2ETestContext ctx; - - @BeforeAll - static void setup() throws Exception { - ctx = E2ETestContext.create(); - } - - @AfterAll - static void teardown() throws Exception { - if (ctx != null) { - ctx.close(); - } - } - - @Test - void testShouldCallRpcPingWithTypedParamsAndResult() throws Exception { - ctx.configureForTest("rpc_server", "should_call_rpc_ping_with_typed_params_and_result"); - - try (var client = ctx.createClient()) { - client.start().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - var result = client.getRpc().ping(new PingParams("typed rpc test")).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertEquals("pong: typed rpc test", result.message()); - assertNotNull(result.timestamp()); - assertNotNull(result.protocolVersion()); - assertTrue(result.protocolVersion() >= 0); - } - } - - @Test - void testShouldClearTheManagedSettingsCache() throws Exception { - ctx.configureForTest("rpc_server", "should_clear_the_managed_settings_cache"); - - try (var client = ctx.createClient()) { - client.start().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - client.getRpc().managedSettings.clearCache().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } - } - - @Test - void testShouldRejectLlmInferenceResponseFramesForMissingRequest() throws Exception { - ctx.initializeProxy(); - - try (var client = ctx.createClient()) { - client.start().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - var requestId = "missing-llm-inference-request"; - - var start = client.getRpc().llmInference - .httpResponseStart(new LlmInferenceHttpResponseStartParams(requestId, 200L, "OK", - Map.of("content-type", List.of("text/event-stream")))) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertFalse(start.accepted()); - - var chunk = client.getRpc().llmInference - .httpResponseChunk( - new LlmInferenceHttpResponseChunkParams(requestId, "data: {}\n\n", false, false, null)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertFalse(chunk.accepted()); - - var error = client.getRpc().llmInference.httpResponseChunk(new LlmInferenceHttpResponseChunkParams( - requestId, "", null, true, - new LlmInferenceHttpResponseChunkError("No pending LLM inference request.", "missing_request"))) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertFalse(error.accepted()); - } - } - - @Test - void testShouldCallRpcModelsListWithTypedResult() throws Exception { - ctx.configureForTest("rpc_server", "should_call_rpc_models_list_with_typed_result"); - var token = "rpc-models-token"; - configureAuthenticatedUser(token, null); - - try (var client = createAuthenticatedClient(token)) { - client.start().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - var result = client.getRpc().models.list().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertNotNull(result.models()); - assertTrue(result.models().stream().anyMatch(model -> "claude-sonnet-5".equals(model.id()))); - result.models().forEach(model -> { - assertFalse(model.id().isBlank()); - assertFalse(model.name().isBlank()); - }); - } - } - - @Test - void testShouldCallRpcAccountGetQuotaWhenAuthenticated() throws Exception { - ctx.configureForTest("rpc_server", "should_call_rpc_account_get_quota_when_authenticated"); - var token = "rpc-quota-token"; - configureAuthenticatedUser(token, Map.of("chat", Map.of("entitlement", 100, "overage_count", 2, - "overage_permitted", true, "percent_remaining", 75, "timestamp_utc", "2026-04-30T00:00:00Z"))); - - try (var client = createAuthenticatedClient(token)) { - client.start().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - var result = client.getRpc().account.getQuota().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertNotNull(result.quotaSnapshots()); - var chatQuota = result.quotaSnapshots().get("chat"); - assertNotNull(chatQuota); - assertQuota(chatQuota); - } - } - - @Test - void testShouldCallRpcToolsListWithTypedResult() throws Exception { - ctx.configureForTest("rpc_server", "should_call_rpc_tools_list_with_typed_result"); - - try (var client = ctx.createClient()) { - client.start().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - var result = client.getRpc().tools.list(new ToolsListParams(null)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertNotNull(result.tools()); - assertFalse(result.tools().isEmpty()); - result.tools().forEach(tool -> assertFalse(tool.name().isBlank())); - } - } - - @Test - void testShouldCallRpcSessionFsSetProviderWithTypedResult() throws Exception { - ctx.initializeProxy(); - - try (var client = ctx.createClient()) { - client.start().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - var result = client.getRpc().sessionFs - .setProvider(new SessionFsSetProviderParams(ctx.getWorkDir().toString(), - ctx.getWorkDir().resolve("session-state").toString(), currentPathConventions(), - new SessionFsSetProviderCapabilities(true))) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertTrue(result.success()); - } - } - - @Test - void testShouldAddSecretFilterValues() throws Exception { - ctx.initializeProxy(); - var env = new HashMap<>(ctx.getEnvironment()); - env.put("COPILOT_ENABLE_SECRET_FILTERING", "true"); - - try (var client = ctx.createClient(new CopilotClientOptions().setEnvironment(env))) { - client.start().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - var secret = "rpc-secret-" + UUID.randomUUID().toString().replace("-", ""); - - var result = client.getRpc().secrets.addFilterValues(new SecretsAddFilterValuesParams(List.of(secret))) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertTrue(result.ok()); - } - } - - @Test - void testShouldListFindAndInspectPersistedSessionState() throws Exception { - ctx.initializeProxy(); - - try (var client = ctx.createClient()) { - var requestedSessionId = UUID.randomUUID().toString(); - var workingDirectory = createUniqueWorkDirectory("server-rpc-list"); - var missingTaskId = "missing-task-" + UUID.randomUUID().toString().replace("-", ""); - var missingSessionId = UUID.randomUUID().toString(); - - try (var session = client.createSession(persistedSessionConfig(requestedSessionId, workingDirectory)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { - var sessionId = session.getSessionId(); - session.log("SERVER_RPC_LIST_READY").get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - saveSession(client, sessionId); - assertNull(client.getRpc().sessions.close(new SessionsCloseParams(sessionId)).get(TIMEOUT_SECONDS, - TimeUnit.SECONDS)); - - var listed = client.getRpc().sessions.list().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertNotNull(listed.sessions()); - - var byPrefix = client.getRpc().sessions - .findByPrefix(new SessionsFindByPrefixParams(sessionId.substring(0, 8))) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertTrue(byPrefix.sessionId() == null || sessionId.equals(byPrefix.sessionId())); - - var byTaskId = client.getRpc().sessions.findByTaskId(new SessionsFindByTaskIdParams(missingTaskId)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertNull(byTaskId.sessionId()); - - var lastForContext = client.getRpc().sessions - .getLastForContext(new SessionsGetLastForContextParams( - new SessionContext(workingDirectory.toString(), null, null, null, null))) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertTrue(lastForContext.sessionId() == null || sessionId.equals(lastForContext.sessionId())); - - var eventFile = client.getRpc().sessions.getEventFilePath(new SessionsGetEventFilePathParams(sessionId)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertTrue(eventFile.filePath().endsWith("events.jsonl")); - - var remoteSteerable = client.getRpc().sessions - .getPersistedRemoteSteerable(new SessionsGetPersistedRemoteSteerableParams(sessionId)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertNull(remoteSteerable.remoteSteerable()); - - var sizes = client.getRpc().sessions.getSizes().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertNotNull(sizes.sizes()); - if (sizes.sizes().containsKey(sessionId)) { - assertTrue(sizes.sizes().get(sessionId) >= 0); - } - - var inUse = client.getRpc().sessions - .checkInUse(new SessionsCheckInUseParams(List.of(sessionId, missingSessionId))) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertNotNull(inUse.inUse()); - assertFalse(inUse.inUse().contains(missingSessionId)); - } - } - } - - @Test - void testShouldEnrichBasicSessionMetadata() throws Exception { - ctx.initializeProxy(); - - try (var client = ctx.createClient()) { - var requestedSessionId = UUID.randomUUID().toString(); - var workingDirectory = createUniqueWorkDirectory("server-rpc-enrich"); - - try (var session = client.createSession(persistedSessionConfig(requestedSessionId, workingDirectory)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { - var sessionId = session.getSessionId(); - session.log("SERVER_RPC_ENRICH_READY").get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - saveSession(client, sessionId); - - var now = OffsetDateTime.now().toString(); - var basic = new LocalSessionMetadataValue(sessionId, now, now, null, "Basic metadata", null, false, - null, new SessionContext(workingDirectory.toString(), null, null, null, null), null); - - var result = client.getRpc().sessions.enrichMetadata(new SessionsEnrichMetadataParams(List.of(basic))) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertNotNull(result.sessions()); - assertEquals(1, result.sessions().size()); - var enriched = result.sessions().get(0); - assertEquals(sessionId, enriched.sessionId()); - assertNotNull(enriched.context()); - assertTrue(pathsEqual(workingDirectory.toString(), enriched.context().cwd())); - assertFalse(enriched.isRemote()); - } - } - } - - @Test - void testShouldCloseActiveSessionAndReleaseLock() throws Exception { - ctx.initializeProxy(); - - try (var client = ctx.createClient()) { - var requestedSessionId = UUID.randomUUID().toString(); - var workingDirectory = createUniqueWorkDirectory("server-rpc-close"); - - try (var session = client.createSession(persistedSessionConfig(requestedSessionId, workingDirectory)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { - var sessionId = session.getSessionId(); - session.log("SERVER_RPC_CLOSE_READY").get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - saveSession(client, sessionId); - - var close = client.getRpc().sessions.close(new SessionsCloseParams(sessionId)).get(TIMEOUT_SECONDS, - TimeUnit.SECONDS); - assertNull(close); - - var release = client.getRpc().sessions.releaseLock(new SessionsReleaseLockParams(sessionId)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertNull(release); - - var inUse = client.getRpc().sessions.checkInUse(new SessionsCheckInUseParams(List.of(sessionId))) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertFalse(inUse.inUse().contains(sessionId)); - } - } - } - - @Test - void testShouldPruneDryRunAndBulkDeletePersistedSession() throws Exception { - ctx.initializeProxy(); - - try (var client = ctx.createClient()) { - var requestedSessionId = UUID.randomUUID().toString(); - var missingSessionId = UUID.randomUUID().toString(); - var workingDirectory = createUniqueWorkDirectory("server-rpc-delete"); - - var session = client.createSession(persistedSessionConfig(requestedSessionId, workingDirectory)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - try { - var sessionId = session.getSessionId(); - saveSession(client, sessionId); - client.getRpc().sessions.close(new SessionsCloseParams(sessionId)).get(TIMEOUT_SECONDS, - TimeUnit.SECONDS); - - var prune = client.getRpc().sessions.pruneOld(new SessionsPruneOldParams(0L, true, true, List.of())) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertTrue(prune.dryRun()); - assertNotNull(prune.candidates()); - assertNotNull(prune.deleted()); - assertFalse(prune.deleted().contains(sessionId)); - assertFalse(prune.candidates().contains(missingSessionId)); - assertNotNull(prune.freedBytes()); - assertTrue(prune.freedBytes() >= 0); - - var delete = client.getRpc().sessions - .bulkDelete(new SessionsBulkDeleteParams(List.of(sessionId, missingSessionId))) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertTrue(delete.freedBytes().containsKey(sessionId)); - assertTrue(delete.freedBytes().get(sessionId) >= 0); - if (delete.freedBytes().containsKey(missingSessionId)) { - assertEquals(0L, delete.freedBytes().get(missingSessionId)); - } - - waitForSessionAbsent(client, sessionId); - } finally { - session.close(); - } - } - } - - @Test - void testShouldSetAdditionalPluginsAndReloadDeferredHooks() throws Exception { - ctx.initializeProxy(); - - try (var client = ctx.createClient()) { - client.start().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertNull(client.getRpc().sessions.setAdditionalPlugins(new SessionsSetAdditionalPluginsParams(List.of())) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS)); - - var requestedSessionId = UUID.randomUUID().toString(); - var workingDirectory = createUniqueWorkDirectory("server-rpc-hooks"); - - try (var session = client.createSession( - persistedSessionConfig(requestedSessionId, workingDirectory).setEnableConfigDiscovery(false)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { - var sessionId = session.getSessionId(); - var reload = client.getRpc().sessions - .reloadPluginHooks(new SessionsReloadPluginHooksParams(sessionId, true)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertNull(reload); - - var loaded = client.getRpc().sessions - .loadDeferredRepoHooks(new SessionsLoadDeferredRepoHooksParams(sessionId)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertNotNull(loaded.startupPrompts()); - assertEquals(0L, loaded.hookCount()); - assertTrue(loaded.startupPrompts().isEmpty()); - } finally { - client.getRpc().sessions.setAdditionalPlugins(new SessionsSetAdditionalPluginsParams(List.of())) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } - } - } - - @Test - void testShouldReportImplementedErrorWhenConnectingUnknownRemoteSession() throws Exception { - ctx.initializeProxy(); - - try (var client = ctx.createClient()) { - client.start().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - var remoteSessionId = "remote-" + UUID.randomUUID().toString().replace("-", ""); - - var ex = assertThrows(Exception.class, () -> client.getRpc().sessions - .connect(new SessionsConnectParams(remoteSessionId)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS)); - var text = ex.toString(); - assertFalse(text.toLowerCase().contains("unhandled method sessions.connect")); - assertTrue(text.toLowerCase().contains("session")); - } - } - - @Test - void testShouldDiscoverServerMcpSkillsAgentsAndInstructions() throws Exception { - ctx.configureForTest("rpc_server", "should_discover_server_mcp_and_skills"); - - try (var client = ctx.createClient()) { - client.start().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - var workDir = ctx.getWorkDir().toString(); - var skillName = "server-rpc-skill-" + UUID.randomUUID().toString().replace("-", ""); - var skillDirectory = createSkillDirectory(skillName, "Skill discovered by server-scoped RPC tests."); - - var mcp = client.getRpc().mcp.discover(new McpDiscoverParams(workDir, null)).get(TIMEOUT_SECONDS, - TimeUnit.SECONDS); - assertNotNull(mcp.servers()); - - var skills = client.getRpc().skills - .discover(new SkillsDiscoverParams(null, List.of(skillDirectory.toString()), null, null)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - var discoveredSkill = findSkill(skills.skills(), skillName); - assertEquals("Skill discovered by server-scoped RPC tests.", discoveredSkill.description()); - assertTrue(discoveredSkill.enabled()); - assertTrue(discoveredSkill.path().replace('\\', '/').endsWith(skillName + "/SKILL.md")); - - var skillPaths = client.getRpc().skills - .getDiscoveryPaths(new SkillsGetDiscoveryPathsParams(List.of(workDir), null, true)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - var projectSkillPath = skillPaths.paths().stream().filter( - path -> pathsEqual(workDir, path.projectPath()) && Boolean.TRUE.equals(path.preferredForCreation())) - .findFirst().orElseThrow(() -> new AssertionError("Expected project skill discovery path")); - assertFalse(projectSkillPath.path().isBlank()); - - var agents = client.getRpc().agents.discover(new AgentsDiscoverParams(List.of(workDir), true)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertNotNull(agents.agents()); - agents.agents().forEach(agent -> assertFalse(agent.name().isBlank())); - - var agentPaths = client.getRpc().agents - .getDiscoveryPaths(new AgentsGetDiscoveryPathsParams(List.of(workDir), true)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - var projectAgentPath = agentPaths.paths().stream().filter( - path -> pathsEqual(workDir, path.projectPath()) && Boolean.TRUE.equals(path.preferredForCreation())) - .findFirst().orElseThrow(() -> new AssertionError("Expected project agent discovery path")); - assertFalse(projectAgentPath.path().isBlank()); - - var instructions = client.getRpc().instructions - .discover(new InstructionsDiscoverParams(List.of(workDir), true)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertNotNull(instructions.sources()); - instructions.sources().forEach(source -> { - assertFalse(source.id().isBlank()); - assertFalse(source.label().isBlank()); - assertFalse(source.sourcePath().isBlank()); - }); - - var instructionPaths = client.getRpc().instructions - .getDiscoveryPaths(new InstructionsGetDiscoveryPathsParams(List.of(workDir), true)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertFalse(instructionPaths.paths().isEmpty()); - assertTrue(instructionPaths.paths().stream().anyMatch(path -> pathsEqual(workDir, path.projectPath()))); - instructionPaths.paths().forEach(path -> assertFalse(path.path().isBlank())); - - try { - client.getRpc().skills.config - .setDisabledSkills(new SkillsConfigSetDisabledSkillsParams(List.of(skillName))) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - var disabledSkills = client.getRpc().skills - .discover(new SkillsDiscoverParams(null, List.of(skillDirectory.toString()), null, null)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - var disabledSkill = findSkill(disabledSkills.skills(), skillName); - assertFalse(disabledSkill.enabled()); - } finally { - client.getRpc().skills.config.setDisabledSkills(new SkillsConfigSetDisabledSkillsParams(List.of())) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } - } - } - - private static CopilotClient createAuthenticatedClient(String token) throws Exception { - return ctx.createClient(new CopilotClientOptions().setGitHubToken(token)); - } - - private static void configureAuthenticatedUser(String token, Map quotaSnapshots) throws Exception { - var user = new HashMap(); - user.put("login", "rpc-user"); - user.put("copilot_plan", "individual_pro"); - user.put("endpoints", Map.of("api", ctx.getProxyUrl(), "telemetry", "https://localhost:1/telemetry")); - user.put("analytics_tracking_id", "rpc-user-tracking-id"); - if (quotaSnapshots != null) { - user.put("quota_snapshots", quotaSnapshots); - } - ctx.setCopilotUserByToken(token, user); - } - - private static void assertQuota(AccountQuotaSnapshot chatQuota) { - assertEquals(100L, chatQuota.entitlementRequests()); - assertEquals(25L, chatQuota.usedRequests()); - assertEquals(75.0, chatQuota.remainingPercentage()); - assertEquals(2.0, chatQuota.overage()); - assertTrue(chatQuota.usageAllowedWithExhaustedQuota()); - assertTrue(chatQuota.overageAllowedWithExhaustedQuota()); - assertEquals(OffsetDateTime.parse("2026-04-30T00:00:00Z"), chatQuota.resetDate()); - } - - private static SessionFsSetProviderConventions currentPathConventions() { - return isWindows() ? SessionFsSetProviderConventions.WINDOWS : SessionFsSetProviderConventions.POSIX; - } - - private static Path createUniqueWorkDirectory(String prefix) throws Exception { - var directory = ctx.getWorkDir().resolve(prefix + "-" + UUID.randomUUID().toString().replace("-", "")); - Files.createDirectories(directory); - return directory; - } - - private static SessionConfig persistedSessionConfig(String sessionId, Path workingDirectory) { - return new SessionConfig().setSessionId(sessionId).setWorkingDirectory(workingDirectory.toString()) - .setInfiniteSessions(new InfiniteSessionConfig().setEnabled(true)) - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL); - } - - private static void saveSession(CopilotClient client, String sessionId) throws Exception { - var save = client.getRpc().sessions.save(new SessionsSaveParams(sessionId)).get(TIMEOUT_SECONDS, - TimeUnit.SECONDS); - assertNull(save); - } - - private static void waitForSessionAbsent(CopilotClient client, String sessionId) throws Exception { - var deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(SESSION_PERSISTENCE_TIMEOUT_MILLIS); - do { - var list = client.getRpc().sessions.list().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertNotNull(list.sessions()); - var present = list.sessions().stream() - .anyMatch(session -> session instanceof Map map && sessionId.equals(map.get("sessionId"))); - if (!present) { - return; - } - Thread.sleep(100); - } while (System.nanoTime() < deadline); - - throw new AssertionError("Timed out waiting for session '" + sessionId + "' to be removed."); - } - - private static Path createSkillDirectory(String skillName, String description) throws Exception { - var skillsDir = ctx.getWorkDir().resolve("server-rpc-skills") - .resolve(UUID.randomUUID().toString().replace("-", "")); - var skillSubdir = skillsDir.resolve(skillName); - Files.createDirectories(skillSubdir); - Files.writeString(skillSubdir.resolve("SKILL.md"), "---\nname: " + skillName + "\ndescription: " + description - + "\n---\n\n# " + skillName + "\n\nThis skill is used by RPC E2E tests.\n"); - return skillsDir; - } - - private static ServerSkill findSkill(List skills, String name) { - return skills.stream().filter(skill -> name.equals(skill.name())).findFirst() - .orElseThrow(() -> new AssertionError("Expected to discover skill " + name)); - } - - private static boolean pathsEqual(String expected, String actual) { - if (actual == null) { - return false; - } - - var expectedPath = Path.of(expected).toAbsolutePath().normalize().toString(); - var actualPath = Path.of(actual).toAbsolutePath().normalize().toString(); - return isWindows() ? expectedPath.equalsIgnoreCase(actualPath) : expectedPath.equals(actualPath); - } - - private static boolean isWindows() { - return System.getProperty("os.name").toLowerCase().contains("win"); - } -} diff --git a/java/sdk/src/test/java/com/github/copilot/RpcServerMiscE2ETest.java b/java/sdk/src/test/java/com/github/copilot/RpcServerMiscE2ETest.java deleted file mode 100644 index 3b06e4d1f7..0000000000 --- a/java/sdk/src/test/java/com/github/copilot/RpcServerMiscE2ETest.java +++ /dev/null @@ -1,133 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -package com.github.copilot; - -import static org.junit.jupiter.api.Assertions.*; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import com.github.copilot.generated.rpc.AccountAllUsers; -import com.github.copilot.generated.rpc.AccountLoginParams; -import com.github.copilot.generated.rpc.AccountLogoutParams; -import com.github.copilot.generated.rpc.UserSettingMetadata; -import com.github.copilot.generated.rpc.UserSettingsSetParams; -import com.github.copilot.rpc.CopilotClientOptions; - -class RpcServerMiscE2ETest { - - private static E2ETestContext ctx; - - @BeforeAll - static void setup() throws Exception { - ctx = E2ETestContext.create(); - } - - @AfterAll - static void teardown() throws Exception { - if (ctx != null) { - ctx.close(); - } - } - - @Test - void testShouldGetSetAndClearUserSettings() throws Exception { - ctx.configureForTest("rpc_server_misc", "should_get_set_and_clear_user_settings"); - - try (var client = ctx.createClient()) { - client.start().get(30, TimeUnit.SECONDS); - var before = client.getRpc().user.settings.get().get(30, TimeUnit.SECONDS); - var entry = before.settings().entrySet().stream().filter(e -> isBooleanSetting(e.getValue())).findFirst() - .orElseThrow(() -> new AssertionError("Expected at least one boolean user setting")); - var key = entry.getKey(); - var original = settingBoolean(entry.getValue()); - var updated = !original; - - var set = client.getRpc().user.settings.set(new UserSettingsSetParams(Map.of(key, updated))).get(30, - TimeUnit.SECONDS); - assertTrue(set.shadowedKeys().isEmpty()); - client.getRpc().user.settings.reload().get(30, TimeUnit.SECONDS); - var afterSet = client.getRpc().user.settings.get().get(30, TimeUnit.SECONDS); - assertEquals(updated, settingBoolean(afterSet.settings().get(key))); - assertFalse(afterSet.settings().get(key).isDefault()); - - var clearSettings = new HashMap(); - clearSettings.put(key, null); - var clear = client.getRpc().user.settings.set(new UserSettingsSetParams(clearSettings)).get(30, - TimeUnit.SECONDS); - assertTrue(clear.shadowedKeys().isEmpty()); - client.getRpc().user.settings.reload().get(30, TimeUnit.SECONDS); - var afterClear = client.getRpc().user.settings.get().get(30, TimeUnit.SECONDS); - assertTrue(afterClear.settings().get(key).isDefault()); - } - } - - @Test - void testShouldLoginListGetCurrentAuthAndLogoutAccount() throws Exception { - ctx.configureForTest("rpc_server_misc", "should_login_list_getcurrentauth_and_logout_account"); - var token = "java-account-token"; - var login = "java-account-user"; - ctx.setCopilotUserByToken(token, login, "individual_pro", ctx.getProxyUrl(), "https://localhost:1/telemetry", - "java-account-tracking-id"); - - var env = new HashMap<>(ctx.getEnvironment()); - env.put("GH_TOKEN", ""); - env.put("GITHUB_TOKEN", ""); - env.put("COPILOT_SDK_AUTH_TOKEN", ""); - - try (var client = new CopilotClient( - new CopilotClientOptions().setCliPath(ctx.getCliPath()).setCwd(ctx.getWorkDir().toString()) - .setEnvironment(env).setGitHubToken("").setUseLoggedInUser(false))) { - client.start().get(30, TimeUnit.SECONDS); - - var initial = client.getRpc().account.getCurrentAuth().get(30, TimeUnit.SECONDS); - assertNull(initial.authInfo()); - - var loginResult = client.getRpc().account.login(new AccountLoginParams("https://github.com", login, token)) - .get(30, TimeUnit.SECONDS); - assertNotNull(loginResult); - - var current = client.getRpc().account.getCurrentAuth().get(30, TimeUnit.SECONDS); - assertNull(current.authErrors()); - assertInstanceOf(Map.class, current.authInfo()); - @SuppressWarnings("unchecked") - var authInfo = (Map) current.authInfo(); - assertEquals(login, authInfo.get("login")); - assertEquals("https://github.com", authInfo.get("host")); - - var users = client.getRpc().account.getAllUsers().get(30, TimeUnit.SECONDS); - users.stream().filter(user -> accountLogin(user).equals(login)).findFirst() - .ifPresent(user -> assertEquals(token, user.token())); - - var logout = client.getRpc().account.logout(new AccountLogoutParams(null, authInfo)).get(30, - TimeUnit.SECONDS); - assertFalse(logout.hasMoreUsers()); - assertNull(client.getRpc().account.getCurrentAuth().get(30, TimeUnit.SECONDS).authInfo()); - } - } - - private static boolean isBooleanSetting(UserSettingMetadata metadata) { - return metadata.value() instanceof Boolean || metadata.default_() instanceof Boolean; - } - - private static boolean settingBoolean(UserSettingMetadata metadata) { - if (metadata.value() instanceof Boolean value) { - return value; - } - return (Boolean) metadata.default_(); - } - - private static String accountLogin(AccountAllUsers user) { - if (user.authInfo() instanceof Map authInfo) { - return String.valueOf(authInfo.get("login")); - } - return ""; - } -} diff --git a/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java b/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java deleted file mode 100644 index d138498840..0000000000 --- a/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java +++ /dev/null @@ -1,155 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -package com.github.copilot; - -import static org.junit.jupiter.api.Assertions.*; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import com.github.copilot.generated.rpc.NamedProviderConfig; -import com.github.copilot.generated.rpc.ProviderConfigType; -import com.github.copilot.generated.rpc.ProviderConfigWireApi; -import com.github.copilot.generated.rpc.ProviderModelConfig; -import com.github.copilot.generated.rpc.SessionCompletionsRequestParams; -import com.github.copilot.generated.rpc.SessionMetadataGetContextHeaviestMessagesParams; -import com.github.copilot.generated.rpc.SessionModelSwitchToParams; -import com.github.copilot.generated.rpc.SessionProviderAddParams; -import com.github.copilot.generated.rpc.SessionToolsUpdateSubagentSettingsParams; -import com.github.copilot.generated.rpc.SessionVisibilitySetParams; -import com.github.copilot.generated.rpc.SessionVisibilityStatus; -import com.github.copilot.generated.rpc.SubagentSettingsEntry; -import com.github.copilot.generated.rpc.SubagentSettingsEntryContextTier; -import com.github.copilot.rpc.MessageOptions; -import com.github.copilot.rpc.PermissionHandler; -import com.github.copilot.rpc.SessionConfig; - -class RpcSessionStateExtrasE2ETest { - - private static E2ETestContext ctx; - - @BeforeAll - static void setup() throws Exception { - ctx = E2ETestContext.create(); - } - - @AfterAll - static void teardown() throws Exception { - if (ctx != null) { - ctx.close(); - } - } - - @Test - void testShouldAddByokProviderAndModelAtRuntime() throws Exception { - ctx.configureForTest("rpc_session_state_extras", "should_add_byok_provider_and_model_at_runtime"); - - try (var client = ctx.createClient()) { - try (var session = client - .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get()) { - var result = session.getRpc().provider - .add(new SessionProviderAddParams(null, - List.of(new NamedProviderConfig("java-e2e-provider", ProviderConfigType.OPENAI, - ProviderConfigWireApi.COMPLETIONS, null, "https://models.example.test/v1", - "provider-key", null, null, Map.of("x-provider", "java"), null)), - List.of(new ProviderModelConfig("small", "java-e2e-provider", null, null, - "Java Added Model", 4096L, null, null, null, null, null)))) - .get(30, TimeUnit.SECONDS); - assertEquals(1, result.models().size()); - - var selectionId = "java-e2e-provider/small"; - session.getRpc().model.switchTo(new SessionModelSwitchToParams(null, selectionId, null, null, null, - null, null, null, null, null, null, null, null, null, null, null)).get(30, TimeUnit.SECONDS); - var current = session.getRpc().model.getCurrent().get(30, TimeUnit.SECONDS); - assertEquals(selectionId, current.modelId()); - } - } - } - - @Test - void testShouldReturnEmptyCompletionsWhenHostDoesNotProvideThem() throws Exception { - ctx.configureForTest("rpc_session_state_extras", - "should_return_empty_completions_when_host_does_not_provide_them"); - - try (var client = ctx.createClient()) { - try (var session = client - .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get()) { - var result = session.getRpc().completions - .request(new SessionCompletionsRequestParams(null, "Use @ to mention context", 5L)) - .get(30, TimeUnit.SECONDS); - assertTrue(result.items().isEmpty()); - } - } - } - - @Test - void testShouldReportVisibilityAsUnsyncedForLocalSession() throws Exception { - ctx.configureForTest("rpc_session_state_extras", "should_report_visibility_as_unsynced_for_local_session"); - - try (var client = ctx.createClient()) { - try (var session = client - .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get()) { - var set = session.getRpc().visibility - .set(new SessionVisibilitySetParams(null, SessionVisibilityStatus.UNSHARED)) - .get(30, TimeUnit.SECONDS); - assertFalse(set.synced()); - assertNull(set.status()); - assertNull(set.shareUrl()); - - var get = session.getRpc().visibility.get().get(30, TimeUnit.SECONDS); - assertFalse(get.synced()); - assertNull(get.status()); - assertNull(get.shareUrl()); - } - } - } - - @Test - void testShouldGetContextAttributionAndHeaviestMessagesAfterTurn() throws Exception { - ctx.configureForTest("rpc_session_state_extras", - "should_get_context_attribution_and_heaviest_messages_after_turn"); - - try (var client = ctx.createClient()) { - try (var session = client - .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get()) { - var answer = session.sendAndWait(new MessageOptions().setPrompt("Say CONTEXT_METADATA_OK exactly.")) - .get(60, TimeUnit.SECONDS); - assertTrue(answer.getData().content().contains("CONTEXT_METADATA_OK")); - - var attribution = session.getRpc().metadata.getContextAttribution().get(30, TimeUnit.SECONDS); - assertNotNull(attribution.contextAttribution()); - var heaviest = session.getRpc().metadata - .getContextHeaviestMessages(new SessionMetadataGetContextHeaviestMessagesParams(null, 5L)) - .get(30, TimeUnit.SECONDS); - assertTrue(heaviest.totalTokens() >= 0); - } - } - } - - @Test - void testShouldUpdateAndClearLiveSubagentSettings() throws Exception { - ctx.configureForTest("rpc_session_state_extras", "should_update_and_clear_live_subagent_settings"); - - try (var client = ctx.createClient()) { - try (var session = client - .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get()) { - session.getRpc().tools.updateSubagentSettings(new SessionToolsUpdateSubagentSettingsParams(null, - new SessionToolsUpdateSubagentSettingsParams.SessionToolsUpdateSubagentSettingsParamsSubagents( - Map.of("general-purpose", - new SubagentSettingsEntry("gpt-5-mini", null, "low", - SubagentSettingsEntryContextTier.LONG_CONTEXT, null)), - List.of("legacy-agent"), null, null))) - .get(30, TimeUnit.SECONDS); - session.getRpc().tools.updateSubagentSettings(new SessionToolsUpdateSubagentSettingsParams(null, null)) - .get(30, TimeUnit.SECONDS); - } - } - } -} diff --git a/java/sdk/src/test/java/com/github/copilot/RpcSmokeE2ETest.java b/java/sdk/src/test/java/com/github/copilot/RpcSmokeE2ETest.java new file mode 100644 index 0000000000..bb2abe016c --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/RpcSmokeE2ETest.java @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.generated.rpc.PingParams; + +class RpcSmokeE2ETest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + @Test + void testShouldCallRpcPingWithTypedParamsAndResult() throws Exception { + ctx.configureForTest("rpc_server", "should_call_rpc_ping_with_typed_params_and_result"); + + try (var client = ctx.createClient()) { + client.start().get(30, TimeUnit.SECONDS); + + var result = client.getRpc().ping(new PingParams("typed rpc test")).get(30, TimeUnit.SECONDS); + + assertEquals("pong: typed rpc test", result.message()); + assertNotNull(result.timestamp()); + assertNotNull(result.protocolVersion()); + assertTrue(result.protocolVersion() >= 0); + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/RpcSurfaceParityE2ETest.java b/java/sdk/src/test/java/com/github/copilot/RpcSurfaceParityE2ETest.java deleted file mode 100644 index b27f60afe4..0000000000 --- a/java/sdk/src/test/java/com/github/copilot/RpcSurfaceParityE2ETest.java +++ /dev/null @@ -1,1203 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -package com.github.copilot; - -import static org.junit.jupiter.api.Assertions.*; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.RecordComponent; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.util.Arrays; -import java.util.Comparator; -import java.util.IdentityHashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.Test; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.copilot.generated.rpc.*; -import com.github.copilot.rpc.CopilotClientOptions; -import com.github.copilot.rpc.PermissionHandler; -import com.github.copilot.rpc.RuntimeConnection; -import com.github.copilot.rpc.SessionConfig; - -@AllowCopilotExperimental -class RpcSurfaceParityE2ETest { - - private static final ObjectMapper MAPPER = new ObjectMapper(); - private static final long TIMEOUT_SECONDS = 30; - private static final int EXPECTED_RPC_METHOD_COUNT = 444; - private static final String EXPECTED_RPC_SIGNATURE_SHA256 = "6e6130e145ea5d9d8892e02aa16211559c68f6164aff2f204d6da46c3d08a91a"; - private static final Map EXPECTED_METHODS_BY_DECLARING_TYPE = Map.ofEntries( - Map.entry("RpcCaller", 2), Map.entry("ServerAccountApi", 6), Map.entry("ServerAccountsApi", 1), - Map.entry("ServerAgentRegistryApi", 1), Map.entry("ServerAgentsApi", 2), Map.entry("ServerCatalogApi", 3), - Map.entry("ServerCommandsApi", 1), Map.entry("ServerExtensionsApi", 3), Map.entry("ServerHooksApi", 1), - // Participant-only host methods are internal; owner - // start/dispose/publishSession retain capture coverage. - Map.entry("ServerHostApi", 3), Map.entry("ServerInstructionsApi", 2), Map.entry("ServerLlmInferenceApi", 3), - Map.entry("ServerManagedSettingsApi", 2), Map.entry("ServerMcpApi", 7), Map.entry("ServerMcpConfigApi", 7), - Map.entry("ServerMcpInstallationsApi", 4), Map.entry("ServerModelsApi", 3), - Map.entry("ServerPluginsApi", 7), Map.entry("ServerPluginsBuiltinApi", 1), - Map.entry("ServerPluginsMarketplacesApi", 6), Map.entry("ServerRpc", 3), Map.entry("ServerRuntimeApi", 1), - Map.entry("ServerSandboxApi", 1), Map.entry("ServerSecretsApi", 1), Map.entry("ServerSessionFsApi", 1), - Map.entry("ServerSessionsApi", 34), Map.entry("ServerSkillsApi", 6), Map.entry("ServerSkillsConfigApi", 2), - Map.entry("ServerSkillsInstallationsApi", 5), Map.entry("ServerToolsApi", 1), - Map.entry("ServerUserSettingsApi", 3), Map.entry("SessionAccountsApi", 3), - Map.entry("SessionAccountsLoginApi", 3), Map.entry("SessionAgentApi", 7), - Map.entry("SessionAutopilotObjectiveApi", 1), Map.entry("SessionCanvasActionApi", 1), - Map.entry("SessionCanvasApi", 4), Map.entry("SessionCanvasProviderApi", 2), - Map.entry("SessionCommandsApi", 8), Map.entry("SessionCompletionsApi", 2), - Map.entry("SessionConnectorsApi", 11), Map.entry("SessionContentExclusionApi", 1), - Map.entry("SessionCustomizationsApi", 1), Map.entry("SessionDebugApi", 1), - Map.entry("SessionDiagnosticsApi", 2), Map.entry("SessionEventLogApi", 4), - Map.entry("SessionExtensionsApi", 5), Map.entry("SessionFleetApi", 1), - Map.entry("SessionGitHubAuthApi", 10), Map.entry("SessionHistoryApi", 10), - Map.entry("SessionInstructionsApi", 2), Map.entry("SessionLimitPredictionApi", 2), - Map.entry("SessionLspApi", 1), Map.entry("SessionManagedSettingsApi", 1), Map.entry("SessionMcpApi", 23), - Map.entry("SessionMcpAppsApi", 6), Map.entry("SessionMcpHeadersApi", 1), Map.entry("SessionMcpOauthApi", 9), - Map.entry("SessionMcpResourcesApi", 3), Map.entry("SessionMetadataApi", 11), Map.entry("SessionModeApi", 2), - Map.entry("SessionModelApi", 8), Map.entry("SessionNameApi", 3), Map.entry("SessionOptionsApi", 1), - Map.entry("SessionPermissionsApi", 10), Map.entry("SessionPermissionsFolderTrustApi", 2), - Map.entry("SessionPermissionsLocationsApi", 3), Map.entry("SessionPermissionsPathsApi", 5), - Map.entry("SessionPermissionsUrlsApi", 1), Map.entry("SessionPlanApi", 5), - Map.entry("SessionPluginsApi", 8), Map.entry("SessionPluginsMarketplacesApi", 6), - Map.entry("SessionProviderApi", 4), Map.entry("SessionQueueApi", 20), Map.entry("SessionRemoteApi", 3), - Map.entry("SessionRpc", 9), Map.entry("SessionSandboxApi", 3), Map.entry("SessionScheduleApi", 9), - Map.entry("SessionSettingsApi", 2), Map.entry("SessionShellApi", 4), Map.entry("SessionSkillsApi", 6), - Map.entry("SessionTasksApi", 13), Map.entry("SessionTelemetryApi", 2), Map.entry("SessionToolsApi", 8), - Map.entry("SessionUiApi", 10), Map.entry("SessionUsageApi", 1), Map.entry("SessionVisibilityApi", 2), - Map.entry("SessionWorkflowApi", 13), Map.entry("SessionWorkflowJournalApi", 2), - Map.entry("SessionWorkspacesApi", 20)); - - @Test - void everyGeneratedRpcMethodHasRequestCaptureCoverageAndStableStructuralInventory() throws Exception { - var caller = new RpcSurfaceTestCli.RecordingCaller(); - var targets = new LinkedHashMap, RpcTarget>(); - collectTargets(new ServerRpc(caller), "", targets, new IdentityHashMap<>()); - collectTargets(new SessionRpc(caller, "surface-session"), "session", targets, new IdentityHashMap<>()); - - var methods = targets.values().stream() - .flatMap(target -> rpcMethods(target.instance().getClass()).stream() - .map(method -> new TargetMethod(target, method))) - .sorted(Comparator.comparing(TargetMethod::signature)).toList(); - var callerMethods = rpcMethods(RpcCaller.class); - Map counts = methods.stream() - .collect(Collectors.groupingBy(method -> method.method().getDeclaringClass().getSimpleName(), - TreeMap::new, Collectors.summingInt(ignored -> 1))); - counts.put(RpcCaller.class.getSimpleName(), callerMethods.size()); - assertEquals(EXPECTED_METHODS_BY_DECLARING_TYPE, counts, - "Generated public RPC methods changed; map each new signature to a capture test or documented exclusion"); - assertEquals(EXPECTED_RPC_METHOD_COUNT, methods.size() + callerMethods.size()); - assertEquals(EXPECTED_RPC_SIGNATURE_SHA256, - sha256(java.util.stream.Stream - .concat(methods.stream().map(TargetMethod::signature), - callerMethods.stream().map(RpcSurfaceParityE2ETest::signature)) - .sorted().collect(Collectors.joining("\n")))); - - for (TargetMethod targetMethod : methods) { - caller.clear(); - var method = targetMethod.method(); - var arguments = Arrays.stream(method.getParameterTypes()).map(RpcSurfaceParityE2ETest::fixture).toArray(); - var future = assertInstanceOf(CompletableFuture.class, - invoke(method, targetMethod.target().instance(), arguments), targetMethod.signature()); - assertNotNull(future); - - var call = assertSingleCall(caller, targetMethod.signature()); - var expectedMethod = targetMethod.target().prefix().isEmpty() - ? method.getName() - : targetMethod.target().prefix() + "." + method.getName(); - assertEquals(expectedMethod, call.method(), targetMethod.signature()); - assertNotNull(call.resultType(), targetMethod.signature()); - if (expectedMethod.startsWith("session.")) { - assertEquals("surface-session", MAPPER.valueToTree(call.params()).path("sessionId").asText(), - targetMethod.signature()); - } - } - } - - @Test - void rpcCallerOverloadsHaveDirectContractCoverage() throws Exception { - var calls = new java.util.concurrent.CopyOnWriteArrayList(); - RpcCaller caller = new RpcCaller() { - @Override - public CompletableFuture invoke(String method, Object params, Class resultType) { - calls.add(new RpcSurfaceTestCli.RecordingCaller.Call(method, params, resultType)); - if (resultType == JsonNode.class) { - return CompletableFuture.completedFuture(resultType.cast(json(""" - {"value":"deserialized"} - """))); - } - return CompletableFuture.completedFuture(null); - } - }; - - caller.invoke("contract.class", Map.of("kind", "class"), Void.class).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - var result = caller - .invoke("contract.javaType", Map.of("kind", "javaType"), - MAPPER.getTypeFactory().constructMapType(Map.class, String.class, String.class)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - caller.invoke("contract.javaTypeVoid", Map.of("kind", "javaTypeVoid"), - MAPPER.getTypeFactory().constructType(Void.class)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertEquals(Map.of("value", "deserialized"), result); - assertEquals(List.of("contract.class", "contract.javaType", "contract.javaTypeVoid"), - calls.stream().map(RpcSurfaceTestCli.RecordingCaller.Call::method).toList()); - assertEquals(List.of(Void.class, JsonNode.class, Void.class), - calls.stream().map(RpcSurfaceTestCli.RecordingCaller.Call::resultType).toList()); - } - - @Test - void omittedNamespaceMethodsUseExactGeneratedEntryPoints() { - var caller = new RpcSurfaceTestCli.RecordingCaller(); - var rpc = new SessionRpc(caller, "direct-session"); - - rpc.permissions.paths.list(); - rpc.permissions.paths.add(params(SessionPermissionsPathsAddParams.class, "{}")); - rpc.permissions.paths.updatePrimary(params(SessionPermissionsPathsUpdatePrimaryParams.class, "{}")); - rpc.permissions.paths.isPathWithinAllowedDirectories( - params(SessionPermissionsPathsIsPathWithinAllowedDirectoriesParams.class, "{}")); - rpc.permissions.paths - .isPathWithinWorkspace(params(SessionPermissionsPathsIsPathWithinWorkspaceParams.class, "{}")); - rpc.permissions.urls.setUnrestrictedMode(params(SessionPermissionsUrlsSetUnrestrictedModeParams.class, "{}")); - - rpc.plan.read(); - rpc.plan.update(params(SessionPlanUpdateParams.class, "{}")); - rpc.plan.delete(); - rpc.plan.readSqlTodos(); - rpc.plan.readSqlTodosWithDependencies(); - - rpc.provider.getEndpoint(); - rpc.provider.getEndpoint(params(SessionProviderGetEndpointParams.class, "{}")); - rpc.provider.add(params(SessionProviderAddParams.class, "{}")); - - rpc.queue.pendingItems(); - rpc.queue.snapshot(); - rpc.queue.moveItem(params(SessionQueueMoveItemParams.class, "{}")); - rpc.queue.insertAt(params(SessionQueueInsertAtParams.class, "{}")); - rpc.queue.removeAt(params(SessionQueueRemoveAtParams.class, "{}")); - rpc.queue.updateText(params(SessionQueueUpdateTextParams.class, "{}")); - rpc.queue.duplicateAt(params(SessionQueueDuplicateAtParams.class, "{}")); - rpc.queue.setDrainPaused(params(SessionQueueSetDrainPausedParams.class, "{}")); - rpc.queue.sendNow(params(SessionQueueSendNowParams.class, "{}")); - rpc.queue.hasPending(); - rpc.queue.beginDeferredIdleDrain(params(SessionQueueBeginDeferredIdleDrainParams.class, "{}")); - rpc.queue.finishDeferredIdleDrain(params(SessionQueueFinishDeferredIdleDrainParams.class, "{}")); - rpc.queue.deferSessionIdle(params(SessionQueueDeferSessionIdleParams.class, "{}")); - rpc.queue.removeMostRecent(); - rpc.queue.clear(); - rpc.queue.consumeSystemNotifications(params(SessionQueueConsumeSystemNotificationsParams.class, "{}")); - rpc.queue.enqueueResumePending(); - rpc.queue.process(); - - rpc.remote.enable(params(SessionRemoteEnableParams.class, "{}")); - rpc.remote.disable(); - rpc.remote.notifySteerableChanged(params(SessionRemoteNotifySteerableChangedParams.class, "{}")); - rpc.sandbox.getEnforcementStatus(); - rpc.sandbox.disableForSession(params(SessionSandboxDisableForSessionParams.class, "{}")); - rpc.sandbox.grantPathForRequest(params(SessionSandboxGrantPathForRequestParams.class, "{}")); - rpc.settings.snapshot(); - rpc.settings.evaluatePredicate(params(SessionSettingsEvaluatePredicateParams.class, "{}")); - rpc.shell.exec(params(SessionShellExecParams.class, "{}")); - rpc.shell.kill(params(SessionShellKillParams.class, "{}")); - rpc.shell.executeUserRequested(params(SessionShellExecuteUserRequestedParams.class, "{}")); - rpc.shell.cancelUserRequested(params(SessionShellCancelUserRequestedParams.class, "{}")); - - rpc.skills.list(); - rpc.skills.getInvoked(); - rpc.skills.enable(params(SessionSkillsEnableParams.class, "{}")); - rpc.skills.disable(params(SessionSkillsDisableParams.class, "{}")); - rpc.skills.reload(); - rpc.skills.ensureLoaded(); - - rpc.tasks.startAgent(params(SessionTasksStartAgentParams.class, "{}")); - rpc.tasks.list(); - rpc.tasks.register(params(SessionTasksRegisterParams.class, "{}")); - rpc.tasks.update(params(SessionTasksUpdateParams.class, "{}")); - rpc.tasks.refresh(); - rpc.tasks.waitForPending(); - rpc.tasks.getProgress(params(SessionTasksGetProgressParams.class, "{}")); - rpc.tasks.getCurrentPromotable(); - rpc.tasks.promoteToBackground(params(SessionTasksPromoteToBackgroundParams.class, "{}")); - rpc.tasks.promoteCurrentToBackground(); - rpc.tasks.cancel(params(SessionTasksCancelParams.class, "{}")); - rpc.tasks.remove(params(SessionTasksRemoveParams.class, "{}")); - rpc.tasks.sendMessage(params(SessionTasksSendMessageParams.class, "{}")); - - rpc.telemetry.getEngagementId(); - rpc.telemetry.setFeatureOverrides(params(SessionTelemetrySetFeatureOverridesParams.class, "{}")); - rpc.tools.execute(params(SessionToolsExecuteParams.class, "{}")); - rpc.tools.getBuiltinDescriptors(params(SessionToolsGetBuiltinDescriptorsParams.class, "{}")); - rpc.tools.taskCompleteEventData(params(SessionToolsTaskCompleteEventDataParams.class, "{}")); - rpc.tools.handlePendingToolCall(params(SessionToolsHandlePendingToolCallParams.class, "{}")); - rpc.tools.initializeAndValidate(); - rpc.tools.getCurrentMetadata(); - rpc.tools.set(params(SessionToolsSetParams.class, "{}")); - rpc.tools.updateSubagentSettings(params(SessionToolsUpdateSubagentSettingsParams.class, "{}")); - - rpc.ui.ephemeralQuery(params(SessionUiEphemeralQueryParams.class, "{}")); - rpc.ui.elicitation(params(SessionUiElicitationParams.class, "{}")); - rpc.ui.handlePendingElicitation(params(SessionUiHandlePendingElicitationParams.class, "{}")); - rpc.ui.handlePendingUserInput(params(SessionUiHandlePendingUserInputParams.class, "{}")); - rpc.ui.handlePendingSampling(params(SessionUiHandlePendingSamplingParams.class, "{}")); - rpc.ui.handlePendingAutoModeSwitch(params(SessionUiHandlePendingAutoModeSwitchParams.class, "{}")); - rpc.ui.handlePendingSessionLimitsExhausted( - params(SessionUiHandlePendingSessionLimitsExhaustedParams.class, "{}")); - rpc.ui.handlePendingExitPlanMode(params(SessionUiHandlePendingExitPlanModeParams.class, "{}")); - rpc.ui.registerDirectAutoModeSwitchHandler(); - rpc.ui.unregisterDirectAutoModeSwitchHandler( - params(SessionUiUnregisterDirectAutoModeSwitchHandlerParams.class, "{}")); - - rpc.visibility.get(); - rpc.visibility.set(params(SessionVisibilitySetParams.class, "{}")); - rpc.workspaces.getWorkspace(); - rpc.workspaces.updateMetadata(params(SessionWorkspacesUpdateMetadataParams.class, "{}")); - rpc.workspaces.ensure(params(SessionWorkspacesEnsureParams.class, "{}")); - rpc.workspaces.listFiles(); - rpc.workspaces.readFile(params(SessionWorkspacesReadFileParams.class, "{}")); - rpc.workspaces.createFile(params(SessionWorkspacesCreateFileParams.class, "{}")); - rpc.workspaces.statFile(params(SessionWorkspacesStatFileParams.class, "{}")); - rpc.workspaces.createDirectory(params(SessionWorkspacesCreateDirectoryParams.class, "{}")); - rpc.workspaces.removePath(params(SessionWorkspacesRemovePathParams.class, "{}")); - rpc.workspaces.renamePath(params(SessionWorkspacesRenamePathParams.class, "{}")); - rpc.workspaces.listCheckpoints(); - rpc.workspaces.readCheckpoint(params(SessionWorkspacesReadCheckpointParams.class, "{}")); - rpc.workspaces.addSummary(params(SessionWorkspacesAddSummaryParams.class, "{}")); - rpc.workspaces.truncateSummaries(params(SessionWorkspacesTruncateSummariesParams.class, "{}")); - rpc.workspaces.readAutopilotObjective(); - rpc.workspaces.writeAutopilotObjective(params(SessionWorkspacesWriteAutopilotObjectiveParams.class, "{}")); - rpc.workspaces.deleteAutopilotObjective(); - rpc.workspaces.autopilotObjectiveExists(); - rpc.workspaces.saveLargePaste(params(SessionWorkspacesSaveLargePasteParams.class, "{}")); - rpc.workspaces.diff(params(SessionWorkspacesDiffParams.class, "{}")); - - assertEquals(105, caller.calls().size()); - assertTrue(caller.calls().stream().allMatch(call -> call.method().startsWith("session."))); - assertTrue(caller.calls().stream().allMatch( - call -> "direct-session".equals(MAPPER.valueToTree(call.params()).path("sessionId").asText()))); - var methods = caller.calls().stream().map(RpcSurfaceTestCli.RecordingCaller.Call::method) - .collect(Collectors.toSet()); - assertTrue(methods.containsAll(Set.of("session.permissions.paths.list", - "session.permissions.urls.setUnrestrictedMode", "session.plan.readSqlTodosWithDependencies", - "session.provider.add", "session.queue.process", "session.remote.notifySteerableChanged", - "session.sandbox.disableForSession", "session.sandbox.grantPathForRequest", - "session.settings.evaluatePredicate", "session.shell.cancelUserRequested", - "session.skills.ensureLoaded", "session.tasks.sendMessage", "session.telemetry.setFeatureOverrides", - "session.tools.updateSubagentSettings", "session.ui.unregisterDirectAutoModeSwitchHandler", - "session.visibility.set", "session.workspaces.diff"))); - } - - @Test - void remainingGeneratedMethodsUseExactEntryPoints() { - var caller = new RpcSurfaceTestCli.RecordingCaller(); - var server = new ServerRpc(caller); - var rpc = new SessionRpc(caller, "remaining-session"); - - rpc.agent.list(params(SessionAgentListParams.class, "{}")); - rpc.autopilotObjective.getState(); - rpc.canvas.action.invoke(params(SessionCanvasActionInvokeParams.class, "{}")); - rpc.canvas.close(params(SessionCanvasCloseParams.class, "{}")); - rpc.canvas.list(); - rpc.canvas.listOpen(); - rpc.canvas.open(params(SessionCanvasOpenParams.class, "{}")); - rpc.canvas.provider.register(params(SessionCanvasProviderRegisterParams.class, "{}")); - rpc.canvas.provider.unregister(params(SessionCanvasProviderUnregisterParams.class, "{}")); - rpc.commands.enqueue(params(SessionCommandsEnqueueParams.class, "{}")); - rpc.commands.execute(params(SessionCommandsExecuteParams.class, "{}")); - rpc.commands.finalizeInvocationEffect(params(SessionCommandsFinalizeInvocationEffectParams.class, "{}")); - rpc.commands.list(params(SessionCommandsListParams.class, "{}")); - rpc.commands.respondToQueuedCommand(params(SessionCommandsRespondToQueuedCommandParams.class, "{}")); - rpc.completions.getTriggerCharacters(); - rpc.eventLog.registerInterest(params(SessionEventLogRegisterInterestParams.class, "{}")); - rpc.eventLog.releaseInterest(params(SessionEventLogReleaseInterestParams.class, "{}")); - rpc.eventLog.tail(); - rpc.extensions.sendAttachmentsToMessage(params(SessionExtensionsSendAttachmentsToMessageParams.class, "{}")); - rpc.workflow.cancel(params(SessionWorkflowCancelParams.class, "{}")); - rpc.workflow.getRunDetail(params(SessionWorkflowGetRunDetailParams.class, "{}")); - rpc.workflow.getRunProgress(params(SessionWorkflowGetRunProgressParams.class, "{}")); - rpc.workflow.listRuns(params(SessionWorkflowListRunsParams.class, "{}")); - rpc.workflow.pauseAtCheckpoint(params(SessionWorkflowPauseAtCheckpointParams.class, "{}")); - rpc.workflow.resumeFromTool(params(SessionWorkflowResumeFromToolParams.class, "{}")); - rpc.workflow.runFromTool(params(SessionWorkflowRunFromToolParams.class, "{}")); - rpc.gitHubAuth.getAllAuthAvailable(); - rpc.gitHubAuth.getCurrentAuthInfo(); - rpc.gitHubAuth.lastAuthErrors(); - rpc.gitHubAuth.login(params(SessionGitHubAuthLoginParams.class, "{}")); - rpc.gitHubAuth.logout(); - rpc.gitHubAuth.logoutUser(params(SessionGitHubAuthLogoutUserParams.class, "{}")); - rpc.gitHubAuth.refreshCopilotUser(); - rpc.gitHubAuth.setCredentials(params(SessionGitHubAuthSetCredentialsParams.class, "{}")); - rpc.gitHubAuth.switchToAuth(params(SessionGitHubAuthSwitchToAuthParams.class, "{}")); - rpc.history.abortManualCompaction(); - rpc.history.cancelBackgroundCompaction(); - rpc.history.compact(params(SessionHistoryCompactParams.class, "{}")); - rpc.history.summarizeForHandoff(); - rpc.instructions.getSources(); - rpc.limitPrediction.predict(); - rpc.lsp.initialize(params(SessionLspInitializeParams.class, "{}")); - rpc.mcp.apps.diagnose(params(SessionMcpAppsDiagnoseParams.class, "{}")); - rpc.mcp.apps.getHostContext(); - rpc.mcp.apps.listTools(params(SessionMcpAppsListToolsParams.class, "{}")); - rpc.mcp.apps.readResource(params(SessionMcpAppsReadResourceParams.class, "{}")); - rpc.mcp.apps.setHostContext(params(SessionMcpAppsSetHostContextParams.class, "{}")); - rpc.mcp.cancelSamplingExecution(params(SessionMcpCancelSamplingExecutionParams.class, "{}")); - rpc.mcp.configureGitHub(params(SessionMcpConfigureGitHubParams.class, "{}")); - rpc.mcp.executeSampling(params(SessionMcpExecuteSamplingParams.class, "{}")); - rpc.mcp.isServerRunning(params(SessionMcpIsServerRunningParams.class, "{}")); - rpc.mcp.oauth.probe(params(SessionMcpOauthProbeParams.class, "{}")); - rpc.mcp.registerExternalClient(params(SessionMcpRegisterExternalClientParams.class, "{}")); - rpc.mcp.reloadWithConfig(params(SessionMcpReloadWithConfigParams.class, "{}")); - rpc.mcp.removeGitHub(); - rpc.mcp.restartServer(params(SessionMcpRestartServerParams.class, "{}")); - rpc.mcp.setEnvValueMode(params(SessionMcpSetEnvValueModeParams.class, "{}")); - rpc.mcp.stopServer(params(SessionMcpStopServerParams.class, "{}")); - rpc.mcp.unregisterExternalClient(params(SessionMcpUnregisterExternalClientParams.class, "{}")); - rpc.metadata.activity(); - rpc.metadata.contextInfo(params(SessionMetadataContextInfoParams.class, "{}")); - rpc.metadata.isProcessing(); - rpc.metadata.recomputeContextTokens(params(SessionMetadataRecomputeContextTokensParams.class, "{}")); - rpc.metadata.recordContextChange(params(SessionMetadataRecordContextChangeParams.class, "{}")); - rpc.metadata.setWorkingDirectory(params(SessionMetadataSetWorkingDirectoryParams.class, "{}")); - rpc.metadata.snapshot(); - rpc.metadata.updateClientMetadata(params(SessionMetadataUpdateClientMetadataParams.class, "{}")); - rpc.model.applyStartupOverlay(params(SessionModelApplyStartupOverlayParams.class, "{}")); - rpc.model.list(); - rpc.model.list(params(SessionModelListParams.class, "{}")); - rpc.model.setReasoningEffort(params(SessionModelSetReasoningEffortParams.class, "{}")); - rpc.name.get(); - rpc.name.set(params(SessionNameSetParams.class, "{}")); - rpc.name.setAuto(params(SessionNameSetAutoParams.class, "{}")); - rpc.options.update(params(SessionOptionsUpdateParams.class, "{}")); - rpc.permissions.configure(params(SessionPermissionsConfigureParams.class, "{}")); - rpc.permissions.folderTrust.addTrusted(params(SessionPermissionsFolderTrustAddTrustedParams.class, "{}")); - rpc.permissions.folderTrust.isTrusted(params(SessionPermissionsFolderTrustIsTrustedParams.class, "{}")); - rpc.permissions.getMode(); - rpc.permissions.locations.addToolApproval(params(SessionPermissionsLocationsAddToolApprovalParams.class, "{}")); - rpc.permissions.locations.apply(params(SessionPermissionsLocationsApplyParams.class, "{}")); - rpc.permissions.locations.resolve(params(SessionPermissionsLocationsResolveParams.class, "{}")); - rpc.permissions.modifyRules(params(SessionPermissionsModifyRulesParams.class, "{}")); - rpc.permissions.notifyPromptShown(params(SessionPermissionsNotifyPromptShownParams.class, "{}")); - rpc.permissions.pendingRequests(); - rpc.permissions.resetSessionApprovals(params(SessionPermissionsResetSessionApprovalsParams.class, "{}")); - rpc.permissions.setMode(params(SessionPermissionsSetModeParams.class, "{}")); - rpc.permissions.setRequired(params(SessionPermissionsSetRequiredParams.class, "{}")); - rpc.plugins.reload(); - rpc.plugins.reload(params(SessionPluginsReloadParams.class, "{}")); - rpc.schedule.add(params(SessionScheduleAddParams.class, "{}")); - rpc.schedule.addAt(params(SessionScheduleAddAtParams.class, "{}")); - rpc.schedule.addCron(params(SessionScheduleAddCronParams.class, "{}")); - rpc.schedule.addSelfPaced(params(SessionScheduleAddSelfPacedParams.class, "{}")); - rpc.schedule.hasSelfPaced(); - rpc.schedule.hydrate(); - rpc.schedule.list(); - rpc.schedule.rearmSelfPaced(params(SessionScheduleRearmSelfPacedParams.class, "{}")); - rpc.schedule.stop(params(SessionScheduleStopParams.class, "{}")); - rpc.sendMessages(params(SessionSendMessagesParams.class, "{}")); - rpc.sendSystemNotification(params(SessionSendSystemNotificationParams.class, "{}")); - rpc.shutdown(params(SessionShutdownParams.class, "{}")); - rpc.suspend(); - - server.account.getQuota(params(AccountGetQuotaParams.class, "{}")); - server.agentRegistry.spawn(params(AgentRegistrySpawnParams.class, "{}")); - server.connect(params(ConnectParams.class, "{}")); - server.extensions.disable(params(ExtensionsDisableParams.class, "{}")); - server.extensions.discover(); - server.extensions.enable(params(ExtensionsEnableParams.class, "{}")); - server.mcp.config.disable(params(McpConfigDisableParams.class, "{}")); - server.mcp.config.enable(params(McpConfigEnableParams.class, "{}")); - server.mcp.config.reload(); - server.models.list(params(ModelsListParams.class, "{}")); - server.plugins.disable(params(PluginsDisableParams.class, "{}")); - server.plugins.enable(params(PluginsEnableParams.class, "{}")); - server.plugins.install(params(PluginsInstallParams.class, "{}")); - server.plugins.list(); - server.plugins.marketplaces.add(params(PluginsMarketplacesAddParams.class, "{}")); - server.plugins.marketplaces.browse(params(PluginsMarketplacesBrowseParams.class, "{}")); - server.plugins.marketplaces.list(); - server.plugins.marketplaces.refresh(); - server.plugins.marketplaces.refresh(params(PluginsMarketplacesRefreshParams.class, "{}")); - server.plugins.marketplaces.remove(params(PluginsMarketplacesRemoveParams.class, "{}")); - server.plugins.uninstall(params(PluginsUninstallParams.class, "{}")); - server.plugins.update(params(PluginsUpdateParams.class, "{}")); - server.plugins.updateAll(); - server.runtime.shutdown(); - server.sessions.configureSessionExtensions(params(SessionsConfigureSessionExtensionsParams.class, "{}")); - server.sessions.delete(params(SessionsDeleteParams.class, "{}")); - server.sessions.getBoardEntryCount(params(SessionsGetBoardEntryCountParams.class, "{}")); - server.sessions.getMetadata(params(SessionsGetMetadataParams.class, "{}")); - server.sessions.getRemoteControlStatus(); - server.sessions.list(params(SessionsListParams.class, "{}")); - server.sessions.listNonEmptySessionIds(params(SessionsListNonEmptySessionIdsParams.class, "{}")); - server.sessions.open((SessionsOpenParams) fixture(SessionsOpenParams.class)); - server.sessions.readPersistedEvents(params(SessionsReadPersistedEventsParams.class, "{}")); - server.sessions.setRemoteControlSteering(params(SessionsSetRemoteControlSteeringParams.class, "{}")); - server.sessions.startRemoteControl(params(SessionsStartRemoteControlParams.class, "{}")); - server.sessions.stopRemoteControl(); - server.sessions.stopRemoteControl(params(SessionsStopRemoteControlParams.class, "{}")); - server.sessions.transferRemoteControl(params(SessionsTransferRemoteControlParams.class, "{}")); - - assertEquals(141, caller.calls().size()); - assertTrue(caller.calls().stream().filter(call -> call.method().startsWith("session.")).allMatch( - call -> "remaining-session".equals(MAPPER.valueToTree(call.params()).path("sessionId").asText()))); - } - - @Test - void protocolErrorsPreserveCodeAndMessage() throws Exception { - try (var runtime = new RpcSurfaceTestCli(request -> { - if ("connect".equals(request.path("method").asText())) { - return json(""" - {"ok":true,"protocolVersion":3,"version":"rpc-surface-test"} - """); - } - if ("runtime.shutdown".equals(request.path("method").asText())) { - return MAPPER.createObjectNode(); - } - throw RpcSurfaceTestCli.error(-32042, "rpc surface rejected", json(""" - {"reason":"policy","retryable":false} - """)); - }); var client = createClient(runtime)) { - client.start().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - var failure = assertThrows(ExecutionException.class, () -> client.getRpc().catalog - .search(params(CatalogSearchParams.class, "{}")).get(TIMEOUT_SECONDS, TimeUnit.SECONDS)); - var rpcFailure = assertInstanceOf(JsonRpcException.class, failure.getCause()); - assertEquals(-32042, rpcFailure.getCode()); - assertEquals("rpc surface rejected", rpcFailure.getMessage()); - assertEquals(1, runtime.requestCount("catalog.search")); - assertTrue(parameters(runtime, "catalog.search").isObject()); - } - } - - @Test - void serverRpcsSerializeRequestsAndProjectNestedResults() throws Exception { - try (var runtime = new RpcSurfaceTestCli(RpcSurfaceParityE2ETest::handle); var client = createClient(runtime)) { - client.start().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - var rpc = client.getRpc(); - - rpc.registerExtensionLaunchProvider().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - var command = rpc.commands.list().get(TIMEOUT_SECONDS, TimeUnit.SECONDS).commands().get(0); - assertEquals("rpc-command", command.name()); - assertEquals(List.of("rpc"), command.aliases()); - assertTrue(command.allowDuringAgentExecution()); - assertTrue(command.schedulable()); - - var hooks = rpc.hooks.discover(params(HooksDiscoverParams.class, """ - {"projectPaths":["Q:\\\\rpc-project"],"excludeHostHooks":true} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertEquals(List.of("rpc-warning"), hooks.warnings()); - assertTrue(hooks.hooks().isEmpty()); - assertTrue(hooks.errors().isEmpty()); - - assertTrue(rpc.llmInference.setProvider().get(TIMEOUT_SECONDS, TimeUnit.SECONDS).success()); - assertEquals("strict", - ((Map) rpc.managedSettings.read().get(TIMEOUT_SECONDS, TimeUnit.SECONDS).settingsJson()) - .get("policy")); - - var plan = assertInstanceOf(McpPlanInstallPlanned.class, - rpc.mcp.planInstall(params(McpPlanInstallParams.class, """ - { - "contract":{"protocolVersion":3,"requiredCapabilities":["mcp-install-planning"]}, - "source":{"kind":"candidate","candidateHandle":"candidate-1","searchId":"search-1"}, - "scope":"user" - } - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS)); - assertEquals("plan-1", plan.getPlan().planHandle()); - assertTrue(plan.getPlan().reloadRequired()); - assertEquals(3L, plan.getNegotiated().runtimeProtocolVersion()); - - assertEquals("built-in-model", - rpc.models.getBuiltInCatalog().get(TIMEOUT_SECONDS, TimeUnit.SECONDS).models().get(0).id()); - rpc.plugins.builtin.set(new PluginsBuiltinSetParams(List.of("Q:\\rpc-plugins"))).get(TIMEOUT_SECONDS, - TimeUnit.SECONDS); - - var metadata = rpc.sessions - .getClientMetadata( - new SessionsGetClientMetadataParams(List.of("persisted-session"), List.of("rpc/key"))) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertEquals("ok", ((Map) metadata.get(0)).get("status")); - assertEquals("rpc-value", ((Map) ((Map) metadata.get(0)).get("metadata")).get("rpc/key")); - - rpc.skills.config.setSkillDisabled(new SkillsConfigSetSkillDisabledParams("skill-one", true)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertCalledOnce(runtime, "registerExtensionLaunchProvider", "commands.list", "hooks.discover", - "llmInference.setProvider", "managedSettings.read", "mcp.planInstall", "models.getBuiltInCatalog", - "plugins.builtin.set", "sessions.getClientMetadata", "skills.config.setSkillDisabled"); - assertTrue(parameters(runtime, "hooks.discover").path("excludeHostHooks").asBoolean()); - assertEquals("candidate", parameters(runtime, "mcp.planInstall").path("source").path("kind").asText()); - assertEquals("skill-one", parameters(runtime, "skills.config.setSkillDisabled").path("name").asText()); - } - } - - @Test - void sessionControlRpcsSerializeRequestsAndProjectUnionsAndState() throws Exception { - try (var runtime = new RpcSurfaceTestCli(RpcSurfaceParityE2ETest::handle); var client = createClient(runtime)) { - client.start().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - try (var session = client - .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { - var rpc = session.getRpc(); - - rpc.agent.setPrompt(new SessionAgentSetPromptParams(null, "agent-1", "Use the RPC prompt.")) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - var exclusion = rpc.contentExclusion - .checkPaths( - new SessionContentExclusionCheckPathsParams(null, List.of("/rpc-workspace/file.txt"))) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertTrue(exclusion.available()); - assertFalse(exclusion.checks().get(0).excluded()); - - var logs = rpc.debug.collectLogs(params(SessionDebugCollectLogsParams.class, """ - { - "destination":{"kind":"directory","outputDirectory":"/rpc-debug"}, - "include":{"events":true,"processLogs":false,"shellLogs":true}, - "additionalEntries":[{"bundlePath":"host/diagnostic.txt","kind":"file", - "path":"/diagnostic.txt","required":true}] - } - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertEquals(DebugCollectLogsResultKind.DIRECTORY, logs.kind()); - assertEquals(123L, logs.entries().get(0).sizeBytes()); - assertEquals("not found", logs.skippedEntries().get(0).reason()); - - assertEquals(4L, rpc.history.clearContext(new SessionHistoryClearContextParams(null, "Reset context.")) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS).messagesCleared()); - - var prediction = assertInstanceOf(SessionLimitPredictionResultUnavailable.class, - rpc.limitPrediction.predict(params(SessionLimitPredictionPredictParams.class, """ - {"request":{"clientType":"sdk","modelId":"model-a"}} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS)); - assertEquals(SessionLimitPredictionUnavailableReason.AUTO_UNRESOLVED, prediction.getReason()); - - var metadata = rpc.metadata.getClientMetadata().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertNotNull(metadata); - - var allowed = rpc.model - .setAllowedModels(new SessionModelSetAllowedModelsParams(null, List.of("model-a", "model-b"))) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertEquals(List.of("model-a", "model-b"), allowed.allowedModels()); - assertEquals("model-a", allowed.fallbackModel()); - - var tier = rpc.model - .switchAutoTier(new SessionModelSwitchAutoTierParams(null, AutoTier.INTELLIGENCE, null)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertEquals(ModelSwitchAutoTierStatus.PENDING, tier.status()); - assertEquals(AutoTier.INTELLIGENCE, tier.effectiveAutoTier()); - assertEquals(AutoTier.BALANCE, tier.supersededAutoTier()); - - var enforcement = rpc.sandbox.getEnforcementStatus().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertTrue(enforcement.required()); - assertFalse(enforcement.blocked()); - assertEquals("managed-policy", enforcement.reason()); - - var disabled = rpc.sandbox - .disableForSession(new SessionSandboxDisableForSessionParams(null, "sandbox-request-1", null)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertTrue(disabled.success()); - assertFalse(disabled.enabled()); - - var granted = rpc.sandbox - .grantPathForRequest( - new SessionSandboxGrantPathForRequestParams(null, "sandbox-request-2", null)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertTrue(granted.success()); - - assertTrue(rpc.abort(new SessionAbortParams(null, AbortReason.USER_INITIATED)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS).success()); - assertTrue(rpc.interruptMainTurn(new SessionInterruptMainTurnParams(null, true)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS).interrupted()); - rpc.cancelAllBackgroundAgents().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - var log = rpc.log(params(SessionLogParams.class, """ - {"message":"RPC log","level":"warning","type":"rpc","ephemeral":true, - "url":"https://example.test/rpc","tip":"Inspect the RPC."} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertEquals("11111111-2222-3333-4444-555555555555", log.eventId().toString()); - - assertCalledOnce(runtime, "session.agent.setPrompt", "session.contentExclusion.checkPaths", - "session.debug.collectLogs", "session.history.clearContext", "session.limitPrediction.predict", - "session.metadata.getClientMetadata", "session.model.setAllowedModels", - "session.model.switchAutoTier", "session.sandbox.getEnforcementStatus", - "session.sandbox.disableForSession", "session.sandbox.grantPathForRequest", "session.abort", - "session.interruptMainTurn", "session.cancelAllBackgroundAgents", "session.log"); - assertEquals(session.getSessionId(), parameters(runtime, "session.log").path("sessionId").asText()); - assertTrue(parameters(runtime, "session.interruptMainTurn").path("flushQueued").asBoolean()); - } - } - } - - @Test - void workflowAndMcpRpcsSerializeRequestsAndProjectStateTransitions() throws Exception { - try (var runtime = new RpcSurfaceTestCli(RpcSurfaceParityE2ETest::handle); var client = createClient(runtime)) { - client.start().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - try (var session = client - .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { - var rpc = session.getRpc(); - - var run = rpc.workflow.run(params(SessionWorkflowRunParams.class, """ - {"name":"rpc-workflow","args":{"input":42}, - "options":{"limits":{"maxAiCredits":2.5,"maxConcurrentSubagents":2, - "maxTotalSubagents":4,"timeoutSeconds":30}, - "logPhaseNames":true,"notifyOnComplete":false}} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertEquals("workflow-run-1", run.runId()); - assertEquals(WorkflowRunStatus.RUNNING, run.status()); - assertEquals(1L, run.attempt()); - - var resumed = rpc.workflow.resume(params(SessionWorkflowResumeParams.class, """ - {"runId":"workflow-run-1","limits":{"maxTotalSubagents":8}, - "notifyOnComplete":true,"logPhaseNames":false} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertEquals("rpc-workflow", resumed.workflowName()); - assertEquals(2L, resumed.run().attempt()); - - assertEquals(WorkflowRunStatus.RUNNING, - rpc.workflow.getRun(new SessionWorkflowGetRunParams(null, "workflow-run-1")) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS).status()); - assertEquals(WorkflowRunStatus.PAUSED, - rpc.workflow.pause(new SessionWorkflowPauseParams(null, "workflow-run-1")) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS).status()); - - rpc.workflow.log(params(SessionWorkflowLogParams.class, """ - {"runId":"workflow-run-1","executionToken":"execution-token-1", - "lines":[{"kind":"log","seq":7,"text":"Workflow progress"}]} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - var agent = rpc.workflow.agent(params(SessionWorkflowAgentParams.class, """ - {"runId":"workflow-run-1","executionToken":"execution-token-1", - "prompt":"Complete the RPC task.", - "options":{"agent":"explore","label":"rpc-agent","model":"model-a", - "reasoningEffort":"high"}} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertEquals("agent-result", ((Map) agent.result()).get("answer")); - - var journal = rpc.workflow.journal.get( - new SessionWorkflowJournalGetParams(null, "workflow-run-1", "execution-token-1", "checkpoint")) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertTrue(journal.hit()); - assertEquals(7, ((Map) journal.resultJson()).get("checkpoint")); - rpc.workflow.journal.put(params(SessionWorkflowJournalPutParams.class, """ - {"runId":"workflow-run-1","executionToken":"execution-token-1", - "key":"checkpoint","resultJson":{"checkpoint":8}} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertTrue( - rpc.mcp.moveLoadingToBackground().get(TIMEOUT_SECONDS, TimeUnit.SECONDS).movedToBackground()); - rpc.mcp.startServer(params(SessionMcpStartServerParams.class, """ - {"serverName":"rpc-server","config":{"command":"node","args":["server.js"]}} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - rpc.mcp.oauth - .authenticationStateChanged( - new SessionMcpOauthAuthenticationStateChangedParams(null, "rpc-server", true)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertTrue(rpc.mcp.oauth.respond(new SessionMcpOauthRespondParams(null, "oauth-request-1")) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS).success()); - - var resources = rpc.mcp.resources - .list(new SessionMcpResourcesListParams(null, "rpc-server", "resource-cursor")) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertEquals("resource-next", resources.nextCursor()); - assertEquals("RPC resource", resources.resources().get(0).name()); - var templates = rpc.mcp.resources - .listTemplates( - new SessionMcpResourcesListTemplatesParams(null, "rpc-server", "template-cursor")) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertEquals("file://rpc/{name}", templates.resourceTemplates().get(0).uriTemplate()); - var content = rpc.mcp.resources - .read(new SessionMcpResourcesReadParams(null, "rpc-server", "file://rpc/resource.txt")) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS).contents().get(0); - assertEquals("resource-content", content.text()); - assertEquals("assistant", content.meta().get("audience")); - - assertCalledOnce(runtime, "session.workflow.run", "session.workflow.resume", "session.workflow.getRun", - "session.workflow.pause", "session.workflow.log", "session.workflow.agent", - "session.workflow.journal.get", "session.workflow.journal.put", - "session.mcp.moveLoadingToBackground", "session.mcp.startServer", - "session.mcp.oauth.authenticationStateChanged", "session.mcp.oauth.respond", - "session.mcp.resources.list", "session.mcp.resources.listTemplates", - "session.mcp.resources.read"); - assertEquals(42, parameters(runtime, "session.workflow.run").path("args").path("input").asInt()); - assertEquals("resource-cursor", - parameters(runtime, "session.mcp.resources.list").path("cursor").asText()); - } - } - } - - @Test - void taskToolAndWorkspaceRpcsSerializeMutationsAndProjectResults() throws Exception { - try (var runtime = new RpcSurfaceTestCli(RpcSurfaceParityE2ETest::handle); var client = createClient(runtime)) { - client.start().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - try (var session = client - .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS)) { - var rpc = session.getRpc(); - - var registered = rpc.tasks.register(params(SessionTasksRegisterParams.class, """ - {"type":"client","clientTaskId":"client-task-1","description":"RPC task", - "cancellable":true,"displayName":"RPC Task"} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertTrue(registered.created()); - assertEquals("task-1", registered.task().id()); - assertEquals("RPC owner", registered.task().owner().displayName()); - - var updated = rpc.tasks.update(params(SessionTasksUpdateParams.class, """ - {"id":"task-1","sequence":1, - "update":{"kind":"progress","message":"Halfway","percentage":50, - "phase":"work","status":"running"}} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertTrue(updated.applied()); - assertEquals(1L, updated.task().sequence()); - - rpc.tools.execute(params(SessionToolsExecuteParams.class, """ - {"name":"rpc_tool","arguments":{"value":"input"},"toolCallId":"tool-call-1"} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - var descriptors = rpc.tools - .getBuiltinDescriptors(params(SessionToolsGetBuiltinDescriptorsParams.class, """ - {"reduceUserIntervention":true,"includeAuthor":true,"skillEmbeddingEnabled":false, - "shellConfig":{"displayName":"PowerShell","shellType":"powershell", - "shellToolName":"shell","listShellsToolName":"list_shells", - "readShellToolName":"read_shell","stopShellToolName":"stop_shell", - "descriptionLines":["Runs shell commands."]}, - "shellSupportsPowerShell7Syntax":true,"shellTimeoutMs":1234, - "backgroundTaskNotificationsEnabled":true} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertEquals("rpc_builtin", descriptors.tools().get(0).name()); - assertEquals(BuiltinToolInputSchemaType.OBJECT, descriptors.tools().get(0).inputSchema().type()); - - rpc.tools.set(params(SessionToolsSetParams.class, """ - {"tools":[{"name":"rpc_external","title":"RPC External", - "description":"External RPC tool","parameters":{"type":"object"}, - "isTerminal":false,"overridesBuiltInTool":false,"skipPermission":true}]} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - var completion = rpc.tools - .taskCompleteEventData(params(SessionToolsTaskCompleteEventDataParams.class, """ - {"arguments":{"objectiveId":17}, - "result":{"resultType":"success","textResultForLlm":"RPC task complete", - "sessionLog":"Completion logged."}} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertEquals(17L, completion.objectiveId()); - assertEquals(TaskCompletionOutcome.COMPLETED, completion.outcome()); - assertTrue(completion.success()); - - var workspace = rpc.workspaces.updateMetadata(params(SessionWorkspacesUpdateMetadataParams.class, """ - {"context":{"owner":"rpc-test"},"name":"Updated RPC workspace"} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertEquals("/rpc-workspace", workspace.path()); - assertEquals("Updated RPC workspace", workspace.workspace().name()); - assertEquals("RPC workspace", rpc.workspaces.ensure(params(SessionWorkspacesEnsureParams.class, """ - {"context":{"owner":"rpc-test"}} - """)).get(TIMEOUT_SECONDS, TimeUnit.SECONDS).workspace().name()); - var stat = rpc.workspaces.statFile(new SessionWorkspacesStatFileParams(null, "folder/file.txt")) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertTrue(stat.isFile()); - assertEquals(42L, stat.size()); - - rpc.workspaces.createDirectory(new SessionWorkspacesCreateDirectoryParams(null, "folder/nested", true)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - rpc.workspaces - .renamePath( - new SessionWorkspacesRenamePathParams(null, "folder/file.txt", "folder/renamed.txt")) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - rpc.workspaces.removePath(new SessionWorkspacesRemovePathParams(null, "folder", true, true)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertEquals("RPC summary", rpc.workspaces - .addSummary(new SessionWorkspacesAddSummaryParams(null, "RPC summary", "Summary content")) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS).summary().get("title")); - assertEquals("Truncated RPC workspace", - rpc.workspaces.truncateSummaries(new SessionWorkspacesTruncateSummariesParams(null, 2L)) - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS).workspace().name()); - - assertCalledOnce(runtime, "session.tasks.register", "session.tasks.update", "session.tools.execute", - "session.tools.getBuiltinDescriptors", "session.tools.set", - "session.tools.taskCompleteEventData", "session.workspaces.updateMetadata", - "session.workspaces.ensure", "session.workspaces.statFile", - "session.workspaces.createDirectory", "session.workspaces.renamePath", - "session.workspaces.removePath", "session.workspaces.addSummary", - "session.workspaces.truncateSummaries"); - assertTrue(parameters(runtime, "session.tools.set").path("tools").get(0).path("skipPermission") - .asBoolean()); - assertTrue(parameters(runtime, "session.workspaces.removePath").path("force").asBoolean()); - } - } - } - - private static CopilotClient createClient(RpcSurfaceTestCli runtime) { - var client = new CopilotClient( - new CopilotClientOptions().setConnection(RuntimeConnection.forInProcess()).setUseLoggedInUser(false)); - client.setInProcessTransportFactory(options -> runtime.open()); - return client; - } - - private static JsonNode handle(JsonNode request) { - String method = request.path("method").asText(); - return switch (method) { - case "connect" -> json(""" - {"ok":true,"protocolVersion":3,"version":"rpc-surface-test"} - """); - case "session.create" -> sessionCreate(request); - case "session.detach" -> json(""" - {"success":true} - """); - case "commands.list" -> json(""" - {"commands":[{"name":"rpc-command","description":"RPC command","aliases":["rpc"], - "allowDuringAgentExecution":true,"experimental":false, - "input":{"hint":"","preserveMultilineInput":false,"required":true}, - "kind":"builtin","schedulable":true}]} - """); - case "hooks.discover" -> json(""" - {"hooks":[],"warnings":["rpc-warning"],"errors":[]} - """); - case "llmInference.setProvider" -> json(""" - {"success":true} - """); - case "managedSettings.read" -> json(""" - {"settingsJson":{"policy":"strict"},"errorMessage":null} - """); - case "mcp.planInstall" -> json(""" - {"kind":"planned","plan":{"planHandle":"plan-1","planHandleExpiresAt":"2026-09-18T12:00:00Z", - "transportChoices":[],"configurationChanges":[],"reloadRequired":true, - "requiresInteractiveConfiguration":false}, - "negotiated":{"runtimeProtocolVersion":3,"grantedCapabilities":[]}} - """); - case "models.getBuiltInCatalog" -> json(""" - {"models":[{"id":"built-in-model","name":"Built-in Model","family":"test-family"}]} - """); - case "sessions.getClientMetadata" -> json(""" - [{"status":"ok","sessionId":"persisted-session","metadata":{"rpc/key":"rpc-value"}}] - """); - case "session.contentExclusion.checkPaths" -> json(""" - {"available":true,"checks":[{"path":"/rpc-workspace/file.txt","excluded":false}]} - """); - case "session.debug.collectLogs" -> json(""" - {"kind":"directory","path":"/rpc-debug", - "entries":[{"bundlePath":"host/diagnostic.txt","sizeBytes":123,"source":"additional"}], - "skippedEntries":[{"bundlePath":"host/missing.txt","path":"/missing.txt","reason":"not found"}]} - """); - case "session.history.clearContext" -> json(""" - {"messagesCleared":4} - """); - case "session.limitPrediction.predict" -> json(""" - {"kind":"unavailable","reason":"auto_unresolved"} - """); - case "session.metadata.getClientMetadata" -> json(""" - {"rpc/key":"rpc-value","rpc/other":"other-value"} - """); - case "session.model.setAllowedModels" -> json(""" - {"allowedModels":["model-a","model-b"],"effectiveAllowedModels":["model-a"], - "fallbackModel":"model-a","modelId":"model-a"} - """); - case "session.model.switchAutoTier" -> json(""" - {"status":"pending","activatingAutoTier":"intelligence","effectiveAutoTier":"intelligence", - "pendingAutoTier":null,"supersededAutoTier":"balance"} - """); - case "session.sandbox.getEnforcementStatus" -> json(""" - {"required":true,"blocked":false,"reason":"managed-policy"} - """); - case "session.sandbox.disableForSession" -> json(""" - {"success":true,"enabled":false} - """); - case "session.sandbox.grantPathForRequest" -> json(""" - {"success":true} - """); - case "session.abort" -> json(""" - {"success":true,"error":null} - """); - case "session.interruptMainTurn" -> json(""" - {"interrupted":true} - """); - case "session.log" -> json(""" - {"eventId":"11111111-2222-3333-4444-555555555555"} - """); - case "session.workflow.run", "session.workflow.getRun" -> json(""" - {"runId":"workflow-run-1","status":"running","attempt":1, - "result":{"value":"running"},"snapshot":{"step":1}} - """); - case "session.workflow.resume" -> json(""" - {"workflowName":"rpc-workflow","run":{"runId":"workflow-run-1","status":"running", - "attempt":2,"snapshot":{"step":3}}} - """); - case "session.workflow.pause" -> json(""" - {"runId":"workflow-run-1","status":"paused","attempt":1, - "reason":"caller requested pause","snapshot":{"step":2}} - """); - case "session.workflow.agent" -> json(""" - {"result":{"answer":"agent-result"}} - """); - case "session.workflow.journal.get" -> json(""" - {"hit":true,"resultJson":{"checkpoint":7}} - """); - case "session.mcp.moveLoadingToBackground" -> json(""" - {"movedToBackground":true} - """); - case "session.mcp.oauth.respond" -> json(""" - {"success":true} - """); - case "session.mcp.resources.list" -> json(""" - {"nextCursor":"resource-next","resources":[{"uri":"file://rpc/resource.txt", - "name":"RPC resource","description":"Resource description","mimeType":"text/plain", - "size":16,"title":"RPC Resource"}]} - """); - case "session.mcp.resources.listTemplates" -> json(""" - {"nextCursor":"template-next","resourceTemplates":[{"uriTemplate":"file://rpc/{name}", - "name":"RPC template","description":"Template description","mimeType":"text/plain", - "title":"RPC Template"}]} - """); - case "session.mcp.resources.read" -> json(""" - {"contents":[{"uri":"file://rpc/resource.txt","mimeType":"text/plain", - "text":"resource-content","blob":null,"_meta":{"audience":"assistant"}}]} - """); - case "session.tasks.register" -> json(""" - {"created":true,"reclaimed":false,"task":{"id":"task-1","type":"client", - "clientTaskId":"client-task-1","description":"RPC task","displayName":"RPC Task", - "activeStartedAt":"2026-09-18T12:00:00.500Z","activeTimeMs":500,"canCancel":true, - "executionMode":"background","owner":{"displayName":"RPC owner","joinId":"join-1", - "kind":"sdk","participantId":"participant-1","presence":"connected","source":"rpc-test"}, - "sequence":0,"status":"running","startedAt":"2026-09-18T12:00:00Z", - "updatedAt":"2026-09-18T12:00:01Z"}} - """); - case "session.tasks.update" -> json(""" - {"applied":true,"duplicate":false,"task":{"id":"task-1","type":"client", - "clientTaskId":"client-task-1","description":"RPC task","displayName":"RPC Task", - "activeTimeMs":500,"canCancel":true,"executionMode":"background", - "owner":{"displayName":"RPC owner","joinId":"join-1","kind":"sdk", - "participantId":"participant-1","presence":"connected","source":"rpc-test"}, - "sequence":1,"status":"running","startedAt":"2026-09-18T12:00:00Z", - "updatedAt":"2026-09-18T12:00:01Z"}} - """); - case "session.tools.getBuiltinDescriptors" -> json(""" - {"tools":[{"name":"rpc_builtin","description":"RPC built-in tool", - "hasSummariseIntention":true,"inputSchema":{"type":"object"}, - "instructions":"Use the RPC built-in.","isTerminal":false,"safeForTelemetry":true, - "title":"RPC Built-in","type":"test"}]} - """); - case "session.tools.taskCompleteEventData" -> json(""" - {"objectiveId":17,"outcome":"completed","reason":"completed","success":true, - "summary":"RPC task complete"} - """); - case "session.workspaces.updateMetadata" -> workspace("Updated RPC workspace"); - case "session.workspaces.ensure" -> workspace("RPC workspace"); - case "session.workspaces.statFile" -> json(""" - {"birthtimeMs":1000,"isDirectory":false,"isFile":true,"mtimeMs":2000,"size":42} - """); - case "session.workspaces.addSummary" -> json(""" - {"summary":{"number":3,"title":"RPC summary","content":"Summary content"}, - "workspace":{"id":"workspace-1","cwd":"/rpc-workspace","name":"RPC workspace"}} - """); - case "session.workspaces.truncateSummaries" -> workspace("Truncated RPC workspace"); - default -> MAPPER.createObjectNode(); - }; - } - - private static JsonNode workspace(String name) { - return json(""" - {"path":"/rpc-workspace","workspace":{"id":"workspace-1","cwd":"/rpc-workspace", - "git_root":"/rpc-workspace","branch":"rpc-branch","name":"%s","client_name":"rpc-client", - "created_at":"2026-09-18T11:00:00Z","remote_steerable":true}} - """.formatted(name)); - } - - private static JsonNode sessionCreate(JsonNode request) { - var result = MAPPER.createObjectNode(); - result.put("sessionId", request.path("params").path("sessionId").asText()); - result.put("workspacePath", "/rpc-workspace"); - result.putNull("capabilities"); - return result; - } - - private static void collectTargets(Object instance, String prefix, Map, RpcTarget> targets, - IdentityHashMap visited) throws IllegalAccessException { - if (visited.put(instance, Boolean.TRUE) != null) { - return; - } - targets.put(instance.getClass(), new RpcTarget(instance, prefix)); - for (var field : instance.getClass().getFields()) { - if (field.getType().getPackageName().equals(ServerRpc.class.getPackageName()) - && field.getType().getSimpleName().endsWith("Api")) { - var child = field.get(instance); - var childPrefix = prefix.isEmpty() ? field.getName() : prefix + "." + field.getName(); - collectTargets(child, childPrefix, targets, visited); - } - } - } - - private static List rpcMethods(Class type) { - return Arrays.stream(type.getDeclaredMethods()).filter(method -> Modifier.isPublic(method.getModifiers())) - .filter(method -> method.getReturnType() == CompletableFuture.class) - .sorted(Comparator.comparing(RpcSurfaceParityE2ETest::signature)).toList(); - } - - private static Object invoke(Method method, Object instance, Object[] arguments) { - try { - return method.invoke(instance, arguments); - } catch (IllegalAccessException e) { - throw new AssertionError("Could not invoke " + signature(method), e); - } catch (InvocationTargetException e) { - throw new AssertionError("Generated wrapper failed before dispatch for " + signature(method), e.getCause()); - } - } - - private static Object fixture(Class type) { - if (type == String.class) { - return "caller-supplied-session"; - } - if (type == boolean.class || type == Boolean.class) { - return true; - } - if (type == byte.class || type == Byte.class) { - return (byte) 1; - } - if (type == short.class || type == Short.class) { - return (short) 1; - } - if (type == int.class || type == Integer.class) { - return 1; - } - if (type == long.class || type == Long.class) { - return 1L; - } - if (type == float.class || type == Float.class) { - return 1F; - } - if (type == double.class || type == Double.class) { - return 1D; - } - if (type.isEnum()) { - return type.getEnumConstants()[0]; - } - if (JsonNode.class.isAssignableFrom(type)) { - return MAPPER.createObjectNode().put("fixture", true); - } - if (List.class.isAssignableFrom(type)) { - return List.of(); - } - if (Map.class.isAssignableFrom(type)) { - return Map.of(); - } - if (type == Object.class) { - return Map.of("fixture", "value"); - } - if (type.isRecord()) { - try { - RecordComponent[] components = type.getRecordComponents(); - var constructor = type.getDeclaredConstructor( - Arrays.stream(components).map(RecordComponent::getType).toArray(Class[]::new)); - var arguments = Arrays.stream(components).map(component -> fixtureComponent(component.getType())) - .toArray(); - return constructor.newInstance(arguments); - } catch (ReflectiveOperationException e) { - throw new AssertionError("Could not construct RPC params " + type.getName(), e); - } - } - var subTypes = type.getAnnotation(com.fasterxml.jackson.annotation.JsonSubTypes.class); - if (subTypes != null && subTypes.value().length > 0) { - return fixture(subTypes.value()[0].value()); - } - var constructors = type.getConstructors(); - if (constructors.length == 1 && constructors[0].getParameterCount() > 0) { - // Extensible request classes take their required inputs as constructor - // arguments. - try { - return constructors[0].newInstance(Arrays.stream(constructors[0].getParameterTypes()) - .map(RpcSurfaceParityE2ETest::fixture).toArray()); - } catch (ReflectiveOperationException e) { - throw new AssertionError("Could not construct RPC request " + type.getName(), e); - } - } - if (!Modifier.isAbstract(type.getModifiers()) && !type.isInterface()) { - try { - return type.getDeclaredConstructor().newInstance(); - } catch (ReflectiveOperationException e) { - throw new AssertionError("Could not construct RPC params " + type.getName(), e); - } - } - throw new AssertionError("Unmapped generated RPC parameter type " + type.getName()); - } - - private static Object fixtureComponent(Class type) { - if (Map.class.isAssignableFrom(type)) { - return null; - } - if (type.isPrimitive() || type == String.class || Number.class.isAssignableFrom(type) || type == Boolean.class - || type.isEnum() || List.class.isAssignableFrom(type) || type == Object.class - || JsonNode.class.isAssignableFrom(type)) { - return fixture(type); - } - return null; - } - - private static RpcSurfaceTestCli.RecordingCaller.Call assertSingleCall(RpcSurfaceTestCli.RecordingCaller caller, - String signature) { - assertEquals(1, caller.calls().size(), signature); - return caller.calls().get(0); - } - - private static String signature(Method method) { - return method.getDeclaringClass().getSimpleName() + "#" + method.getName() + "(" - + Arrays.stream(method.getParameterTypes()).map(Class::getSimpleName).collect(Collectors.joining(",")) - + ")"; - } - - private static String sha256(String value) { - try { - return java.util.HexFormat.of() - .formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); - } catch (java.security.NoSuchAlgorithmException e) { - throw new AssertionError(e); - } - } - - private record RpcTarget(Object instance, String prefix) { - } - - private record TargetMethod(RpcTarget target, Method method) { - - String signature() { - return RpcSurfaceParityE2ETest.signature(method); - } - } - - private static T params(Class type, String json) { - try { - return MAPPER.readValue(json, type); - } catch (JsonProcessingException e) { - throw new AssertionError("Invalid test parameters for " + type.getSimpleName(), e); - } - } - - private static JsonNode json(String json) { - try { - return MAPPER.readTree(json); - } catch (JsonProcessingException e) { - throw new AssertionError("Invalid fake runtime JSON", e); - } - } - - private static JsonNode parameters(RpcSurfaceTestCli runtime, String method) { - return runtime.request(method).path("params"); - } - - private static void assertCalledOnce(RpcSurfaceTestCli runtime, String... methods) { - for (String method : methods) { - assertEquals(1, runtime.requestCount(method), "Unexpected call count for " + method); - if (method.startsWith("session.")) { - assertFalse(parameters(runtime, method).path("sessionId").asText().isBlank(), - "Expected sessionId for " + method); - } - } - } -} diff --git a/java/sdk/src/test/java/com/github/copilot/RpcSurfaceTestCli.java b/java/sdk/src/test/java/com/github/copilot/RpcSurfaceTestCli.java deleted file mode 100644 index d51abec631..0000000000 --- a/java/sdk/src/test/java/com/github/copilot/RpcSurfaceTestCli.java +++ /dev/null @@ -1,247 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -package com.github.copilot; - -import static org.junit.jupiter.api.Assertions.fail; - -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.channels.Channels; -import java.nio.channels.Pipe; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; - -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.NullNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.github.copilot.generated.rpc.RpcCaller; - -final class RpcSurfaceTestCli implements AutoCloseable { - - private static final ObjectMapper MAPPER = new ObjectMapper(); - - record RpcError(int code, String message, JsonNode data) { - } - - static final class RecordingCaller implements RpcCaller { - - record Call(String method, Object params, Object resultType) { - } - - private final List calls = new CopyOnWriteArrayList<>(); - - @Override - public CompletableFuture invoke(String method, Object params, Class resultType) { - calls.add(new Call(method, params, resultType)); - return CompletableFuture.completedFuture(null); - } - - @Override - public CompletableFuture invoke(String method, Object params, JavaType resultType) { - calls.add(new Call(method, params, resultType)); - return CompletableFuture.completedFuture(null); - } - - List calls() { - return List.copyOf(calls); - } - - void clear() { - calls.clear(); - } - } - - private final Function handler; - private final List requests = new CopyOnWriteArrayList<>(); - private final AtomicBoolean closed = new AtomicBoolean(); - private final AtomicReference responderFailure = new AtomicReference<>(); - private final BytePipe toClient; - private final BytePipe toRuntime; - private final InputStream runtimeInput; - private final OutputStream runtimeOutput; - private final Thread responder; - - RpcSurfaceTestCli(Function handler) throws IOException { - this.handler = handler; - this.toClient = new BytePipe(); - this.toRuntime = new BytePipe(); - this.runtimeInput = toRuntime.inputStream(); - this.runtimeOutput = toClient.outputStream(); - this.responder = new Thread(this::respondToRequests, "fake-rpc-runtime"); - this.responder.setDaemon(true); - this.responder.start(); - } - - CopilotClient.InProcessTransport open() { - return new CopilotClient.InProcessTransport(toClient.inputStream(), toRuntime.outputStream(), this::close); - } - - JsonNode request(String method) { - return requests.stream().filter(request -> method.equals(request.path("method").asText())).findFirst() - .orElseGet(() -> { - fail("Expected request for " + method + "; captured methods were " - + requests.stream().map(request -> request.path("method").asText()).toList()); - return NullNode.getInstance(); - }); - } - - long requestCount(String method) { - return requests.stream().filter(request -> method.equals(request.path("method").asText())).count(); - } - - @Override - public void close() { - if (!closed.compareAndSet(false, true)) { - assertResponderHealthy(); - return; - } - toRuntime.close(); - toClient.close(); - responder.interrupt(); - assertResponderHealthy(); - } - - private void respondToRequests() { - try { - while (!closed.get()) { - JsonNode request = readMessage(runtimeInput); - if (request == null) { - return; - } - requests.add(request.deepCopy()); - if (!request.hasNonNull("id")) { - continue; - } - - ObjectNode response = MAPPER.createObjectNode(); - response.put("jsonrpc", "2.0"); - response.set("id", request.get("id")); - try { - JsonNode result = handler.apply(request); - response.set("result", result == null ? NullNode.getInstance() : result); - } catch (RpcErrorException error) { - ObjectNode errorNode = response.putObject("error"); - errorNode.put("code", error.error.code()); - errorNode.put("message", error.error.message()); - if (error.error.data() != null) { - errorNode.set("data", error.error.data()); - } - } catch (Throwable failure) { - responderFailure.compareAndSet(null, failure); - ObjectNode errorNode = response.putObject("error"); - errorNode.put("code", -32603); - errorNode.put("message", "Fake runtime handler failed: " + failure.getMessage()); - } - writeMessage(runtimeOutput, response); - } - } catch (IOException e) { - if (!closed.get()) { - responderFailure.compareAndSet(null, e); - } - } - } - - private void assertResponderHealthy() { - Throwable failure = responderFailure.get(); - if (failure != null) { - throw new AssertionError("Fake runtime failed", failure); - } - } - - static RuntimeException error(int code, String message, JsonNode data) { - return new RpcErrorException(new RpcError(code, message, data)); - } - - private static JsonNode readMessage(InputStream in) throws IOException { - int contentLength = -1; - var line = new ByteArrayOutputStream(); - while (true) { - int b = in.read(); - if (b == -1) { - return null; - } - if (b == '\n') { - String header = line.toString(StandardCharsets.UTF_8).trim(); - line.reset(); - if (header.isEmpty()) { - break; - } - if (header.toLowerCase(Locale.ROOT).startsWith("content-length:")) { - try { - contentLength = Integer.parseInt(header.substring(header.indexOf(':') + 1).trim()); - } catch (NumberFormatException e) { - throw new IOException("Invalid Content-Length header: " + header, e); - } - } - } else if (b != '\r') { - line.write(b); - } - } - if (contentLength < 0) { - throw new IOException("Missing Content-Length header"); - } - byte[] body = in.readNBytes(contentLength); - return body.length == contentLength ? MAPPER.readTree(body) : null; - } - - private static void writeMessage(OutputStream out, JsonNode message) throws IOException { - byte[] body = MAPPER.writeValueAsBytes(message); - out.write(("Content-Length: " + body.length + "\r\n\r\n").getBytes(StandardCharsets.UTF_8)); - out.write(body); - out.flush(); - } - - private static final class RpcErrorException extends RuntimeException { - - private static final long serialVersionUID = 1L; - private final RpcError error; - - RpcErrorException(RpcError error) { - super(error.message()); - this.error = error; - } - } - - private static final class BytePipe { - - private final Pipe pipe; - - BytePipe() throws IOException { - this.pipe = Pipe.open(); - } - - InputStream inputStream() { - return Channels.newInputStream(pipe.source()); - } - - OutputStream outputStream() { - return Channels.newOutputStream(pipe.sink()); - } - - void close() { - closeQuietly(pipe.sink()); - closeQuietly(pipe.source()); - } - - private static void closeQuietly(Closeable closeable) { - try { - closeable.close(); - } catch (IOException e) { - // Nothing useful to do while tearing down a test pipe. - } - } - } -} diff --git a/java/sdk/src/test/java/com/github/copilot/RpcTasksAndHandlersE2ETest.java b/java/sdk/src/test/java/com/github/copilot/RpcTasksAndHandlersE2ETest.java deleted file mode 100644 index 89b283339e..0000000000 --- a/java/sdk/src/test/java/com/github/copilot/RpcTasksAndHandlersE2ETest.java +++ /dev/null @@ -1,72 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -package com.github.copilot; - -import static org.junit.jupiter.api.Assertions.*; - -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import com.github.copilot.generated.rpc.SessionMcpHeadersHandlePendingHeadersRefreshRequestParams; -import com.github.copilot.generated.rpc.SessionUiHandlePendingSessionLimitsExhaustedParams; -import com.github.copilot.generated.rpc.UISessionLimitsExhaustedResponse; -import com.github.copilot.generated.rpc.UISessionLimitsExhaustedResponseAction; -import com.github.copilot.rpc.PermissionHandler; -import com.github.copilot.rpc.SessionConfig; - -class RpcTasksAndHandlersE2ETest { - - private static E2ETestContext ctx; - - @BeforeAll - static void setup() throws Exception { - ctx = E2ETestContext.create(); - } - - @AfterAll - static void teardown() throws Exception { - if (ctx != null) { - ctx.close(); - } - } - - @Test - void testShouldReturnExpectedResultsForMissingPendingHandlerRequestIds() throws Exception { - ctx.initializeProxy(); - - try (var client = ctx.createClient()) { - try (var session = client - .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get()) { - var sessionLimits = session.getRpc().ui - .handlePendingSessionLimitsExhausted( - new SessionUiHandlePendingSessionLimitsExhaustedParams(null, - "missing-session-limits-request", - new UISessionLimitsExhaustedResponse( - UISessionLimitsExhaustedResponseAction.UNSET, null, null))) - .get(30, TimeUnit.SECONDS); - assertFalse(sessionLimits.success()); - - var headersRefresh = session.getRpc().mcp.headers - .handlePendingHeadersRefreshRequest( - new SessionMcpHeadersHandlePendingHeadersRefreshRequestParams(null, - "missing-headers-refresh-request", - Map.of("kind", "headers", "headers", Map.of("x-refresh", "missing")))) - .get(30, TimeUnit.SECONDS); - assertFalse(headersRefresh.success()); - - var noHeadersRefresh = session.getRpc().mcp.headers - .handlePendingHeadersRefreshRequest( - new SessionMcpHeadersHandlePendingHeadersRefreshRequestParams(null, - "missing-headers-refresh-none-request", Map.of("kind", "none"))) - .get(30, TimeUnit.SECONDS); - assertFalse(noHeadersRefresh.success()); - } - } - } -} diff --git a/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java b/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java index 396d076ff5..774ae10164 100644 --- a/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java +++ b/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java @@ -6,15 +6,30 @@ import static org.junit.jupiter.api.Assertions.*; +import java.lang.reflect.RecordComponent; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; +import com.github.copilot.generated.rpc.AuthIdentityMetadata; +import com.github.copilot.generated.rpc.AuthInfoType; +import com.github.copilot.generated.rpc.ConnectorAvailability; +import com.github.copilot.generated.rpc.ConnectorSessionAccount; import com.github.copilot.generated.rpc.McpConfigAddParams; import com.github.copilot.generated.rpc.McpDiscoverParams; import com.github.copilot.generated.rpc.RpcCaller; @@ -29,6 +44,7 @@ import com.github.copilot.generated.rpc.SessionConnectorsListParams; import com.github.copilot.generated.rpc.SessionConnectorsListResult; import com.github.copilot.generated.rpc.SessionConnectorsReconcileParams; +import com.github.copilot.generated.rpc.SessionConnectorsReconcileRequest; import com.github.copilot.generated.rpc.SessionConnectorsReconcileResult; import com.github.copilot.generated.rpc.SessionConnectorsReconnectParams; import com.github.copilot.generated.rpc.SessionConnectorsRefreshParams; @@ -249,6 +265,159 @@ void sessionRpc_connectors_exposes_host_lifecycle_methods() { var reconcileParams = assertConnectorCall(stub.calls.get(8), "session.connectors.reconcile", SessionConnectorsReconcileResult.class); assertTrue(reconcileParams.get("refreshCatalog").asBoolean()); + assertFalse(reconcileParams.has("forceConnectorName")); + assertEquals(3, reconcileParams.size()); + } + + @ParameterizedTest + @CsvSource(value = {"enabled, missing, missing, false, false", "enabled, false, false, false, false", + "enabled, true, missing, true, false", "enabled, missing, true, false, true", + "enabled, true, false, true, false", "enabled, false, true, false, true", "enabled, true, true, true, true", + "disabled, true, true, false, false", "unavailable, true, true, false, false"}, nullValues = "missing") + @AllowCopilotExperimental + void sessionRpc_connectors_capability_gates_preserve_legacy_requests(String availability, + Boolean sessionAccountSelection, Boolean targetedReconcile, boolean expectAccount, boolean expectTarget) + throws Exception { + var mapper = new ObjectMapper(); + var capabilitiesJson = mapper.createObjectNode().put("apiVersion", 1).put("availability", availability) + .put("consentContinuation", true).put("opaqueAccountSelection", true).put("maxPollAttempts", 5) + .put("maxPollIntervalMs", 2_000).put("maxDeadlineMs", 30_000); + if (sessionAccountSelection != null) { + capabilitiesJson.put("sessionAccountSelection", sessionAccountSelection); + } + if (targetedReconcile != null) { + capabilitiesJson.put("targetedReconcile", targetedReconcile); + } + var stub = new StubCaller(); + stub.nextResult = mapper.treeToValue(capabilitiesJson, SessionConnectorsGetCapabilitiesResult.class); + var session = new SessionRpc(stub, "sess-connectors"); + + var capabilities = session.connectors.getCapabilities().get(); + assertEquals(sessionAccountSelection, capabilities.sessionAccountSelection()); + assertEquals(targetedReconcile, capabilities.targetedReconcile()); + stub.nextResult = null; + + if (capabilities.availability() == ConnectorAvailability.ENABLED + && Boolean.TRUE.equals(capabilities.sessionAccountSelection())) { + session.connectors.getAccount(); + } + if (capabilities.availability() == ConnectorAvailability.ENABLED) { + session.connectors.reconcile(new SessionConnectorsReconcileRequest("account-1").setRefreshCatalog(false) + .setForceConnectorName(Boolean.TRUE.equals(capabilities.targetedReconcile()) ? "outlook" : null)); + } + + boolean enabled = "enabled".equals(availability); + assertEquals(1 + (expectAccount ? 1 : 0) + (enabled ? 1 : 0), stub.calls.size()); + var capabilityParams = assertConnectorCall(stub.calls.get(0), "session.connectors.getCapabilities", + SessionConnectorsGetCapabilitiesResult.class); + assertEquals(1, capabilityParams.size()); + int nextCall = 1; + if (expectAccount) { + var params = assertConnectorCall(stub.calls.get(nextCall++), "session.connectors.getAccount", + ConnectorSessionAccount.class); + assertEquals(mapper.valueToTree(Map.of("sessionId", "sess-connectors")), params); + } + if (enabled) { + var params = assertConnectorCall(stub.calls.get(nextCall), "session.connectors.reconcile", + SessionConnectorsReconcileResult.class); + assertEquals("account-1", params.get("accountId").asText()); + assertFalse(params.get("refreshCatalog").asBoolean()); + assertEquals(expectTarget, params.has("forceConnectorName")); + assertEquals(expectTarget ? 4 : 3, params.size()); + if (expectTarget) { + assertEquals("outlook", params.get("forceConnectorName").asText()); + } + } + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"token", "token-provider"}) + @AllowCopilotExperimental + void sessionRpc_connectors_getAccount_preserves_present_and_null_results(String authType) throws Exception { + var mapper = new ObjectMapper(); + try (var sockets = new SocketPair()) { + var stub = sockets.stubServer(); + var session = new SessionRpc(sockets.client()::invoke, "sess-connectors"); + var capabilitiesFuture = session.connectors.getCapabilities(); + var capabilitiesRequest = stub.readOneMessage(); + assertEquals("session.connectors.getCapabilities", capabilitiesRequest.get("method").asText()); + stub.respond(capabilitiesRequest, mapper.readTree(""" + { + "apiVersion": 1, + "availability": "enabled", + "consentContinuation": true, + "opaqueAccountSelection": true, + "sessionAccountSelection": true, + "maxPollAttempts": 5, + "maxPollIntervalMs": 2000, + "maxDeadlineMs": 30000 + } + """)); + var capabilities = capabilitiesFuture.get(3, TimeUnit.SECONDS); + assertEquals(ConnectorAvailability.ENABLED, capabilities.availability()); + assertTrue(Boolean.TRUE.equals(capabilities.sessionAccountSelection())); + + var future = session.connectors.getAccount(); + var request = stub.readOneMessage(); + assertEquals("session.connectors.getAccount", request.get("method").asText()); + assertEquals(mapper.valueToTree(Map.of("sessionId", "sess-connectors")), request.get("params")); + var expected = authType == null ? mapper.getNodeFactory().nullNode() : mapper.readTree(""" + { + "accountId": "account-1", + "authInfo": {"type":"%s","host":"github.com","login":"octocat"} + } + """.formatted(authType)); + stub.respond(request, expected); + ConnectorSessionAccount account = future.get(3, TimeUnit.SECONDS); + + if (authType == null) { + assertNull(account); + } else { + assertNotNull(account); + assertEquals("account-1", account.accountId()); + AuthIdentityMetadata authInfo = account.authInfo(); + assertEquals(AuthInfoType.fromValue(authType), authInfo.type()); + assertEquals("github.com", authInfo.host()); + assertEquals("octocat", authInfo.login()); + assertEquals(expected, mapper.valueToTree(account)); + assertEquals(List.of("accountId", "authInfo"), + Arrays.stream(ConnectorSessionAccount.class.getRecordComponents()).map(RecordComponent::getName) + .sorted().toList()); + assertEquals(List.of("host", "login", "type"), + Arrays.stream(AuthIdentityMetadata.class.getRecordComponents()).map(RecordComponent::getName) + .sorted().toList()); + } + } + } + + @Test + @AllowCopilotExperimental + void sessionRpc_connectors_unknown_continuation_outcome_is_a_decode_error() throws Exception { + try (var sockets = new SocketPair()) { + var stub = sockets.stubServer(); + var session = new SessionRpc(sockets.client()::invoke, "sess-connectors"); + var future = session.connectors.continueConnection(new SessionConnectorsContinueConnectionParams( + "ignored-session", "continuation-1", 3L, 1_000L, 10_000L)); + var request = stub.readOneMessage(); + assertEquals("session.connectors.continueConnection", request.get("method").asText()); + assertEquals(new ObjectMapper().readTree(""" + { + "sessionId":"sess-connectors", + "continuationId":"continuation-1", + "maxAttempts":3, + "pollIntervalMs":1000, + "deadlineMs":10000 + } + """), request.get("params")); + stub.respond(request, new ObjectMapper().readTree(""" + {"kind":"verification_required","continuationId":"continuation-1"} + """)); + + var failure = assertThrows(ExecutionException.class, () -> future.get(3, TimeUnit.SECONDS)); + var decodeError = assertInstanceOf(InvalidTypeIdException.class, failure.getCause()); + assertEquals("verification_required", decodeError.getTypeId()); + } } @Test @@ -580,22 +749,35 @@ public void close() throws Exception { } /** - * Reads raw JSON-RPC messages written to the server side of the socket. + * Exchanges raw JSON-RPC messages on the server side of the socket. */ private static final class StubServer { private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); private final java.io.InputStream in; + private final java.io.OutputStream out; StubServer(java.net.Socket socket) { try { this.in = socket.getInputStream(); + this.out = socket.getOutputStream(); } catch (Exception e) { throw new RuntimeException(e); } } + void respond(JsonNode request, JsonNode result) throws Exception { + var response = MAPPER.createObjectNode(); + response.put("jsonrpc", "2.0"); + response.set("id", request.get("id")); + response.set("result", result); + byte[] body = MAPPER.writeValueAsBytes(response); + out.write(("Content-Length: " + body.length + "\r\n\r\n").getBytes(StandardCharsets.US_ASCII)); + out.write(body); + out.flush(); + } + /** * Reads one JSON-RPC message (Content-Length framed) from the stream. */ diff --git a/java/sdk/src/test/java/com/github/copilot/RuntimeHostIT.java b/java/sdk/src/test/java/com/github/copilot/RuntimeHostIT.java index 94a6c8da11..43b286da48 100644 --- a/java/sdk/src/test/java/com/github/copilot/RuntimeHostIT.java +++ b/java/sdk/src/test/java/com/github/copilot/RuntimeHostIT.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.*; import com.fasterxml.jackson.databind.JsonNode; +import com.github.copilot.generated.rpc.HostLocalServerOptions; import com.github.copilot.rpc.PermissionHandler; import com.github.copilot.rpc.ResumeSessionConfig; import com.github.copilot.rpc.SessionConfig; @@ -93,27 +94,28 @@ ResumeSessionConfig configure(ResumeSessionConfig config) { } AhpHostOptions options() { - return new AhpHostOptions().setCreateSession(request -> { - creates.incrementAndGet(); - assertFalse(request.cancellation().toCompletableFuture().isDone()); - assertEquals(context.getWorkDir().toString(), request.config().getWorkingDirectory()); - return client.createSession(configure(request.config())).thenApply(value -> session = value); - }).setResumeSession(request -> { - resumes.incrementAndGet(); - assertFalse(request.cancellation().toCompletableFuture().isDone()); - assertEquals(false, request.config().getContinuePendingWork().orElseThrow()); - assertEquals(context.getWorkDir().toString(), request.config().getWorkingDirectory()); - return client.resumeSession(request.sessionId(), configure(request.config())) - .thenApply(value -> session = value); - }).setOnSessionReleased(value -> { - releases.add(value); - released.complete(null); - return CompletableFuture.completedFuture(null); - }).setOnExit(info -> { - exits.incrementAndGet(); - exited.complete(null); - return CompletableFuture.completedFuture(null); - }); + return new AhpHostOptions().setLocalServer(new HostLocalServerOptions(null, null, null, null)) + .setCreateSession(request -> { + creates.incrementAndGet(); + assertFalse(request.cancellation().toCompletableFuture().isDone()); + assertEquals(context.getWorkDir().toString(), request.config().getWorkingDirectory()); + return client.createSession(configure(request.config())).thenApply(value -> session = value); + }).setResumeSession(request -> { + resumes.incrementAndGet(); + assertFalse(request.cancellation().toCompletableFuture().isDone()); + assertEquals(false, request.config().getContinuePendingWork().orElseThrow()); + assertEquals(context.getWorkDir().toString(), request.config().getWorkingDirectory()); + return client.resumeSession(request.sessionId(), configure(request.config())) + .thenApply(value -> session = value); + }).setOnSessionReleased(value -> { + releases.add(value); + released.complete(null); + return CompletableFuture.completedFuture(null); + }).setOnExit(info -> { + exits.incrementAndGet(); + exited.complete(null); + return CompletableFuture.completedFuture(null); + }); } void assertCallbacks(String sessionId) throws Exception { @@ -132,6 +134,7 @@ void assertReleased() throws Exception { } private static String connect(AhpTestClient ahp, AhpHost host, String clientId) throws Exception { + assertNotNull(host.getUrl()); var command = new HashMap( Map.of("op", "connect", "url", host.getUrl(), "githubToken", "fake-token-for-e2e-tests")); if (host.getToken() != null) { diff --git a/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java b/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java index 30b04fd972..46b71d7d3f 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java @@ -21,6 +21,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; @@ -85,6 +86,35 @@ void testGenericEventHandler() { assertInstanceOf(SessionIdleEvent.class, receivedEvents.get(2)); } + @Test + void testHandlerAddedDuringDispatchStartsWithNextEvent() { + var received = new ArrayList(); + // Fixed hashes make the old weakly consistent iterator visit the new handler + // after the registering handlers, without relying on scheduling or identity + // hashes. + record Handler(int hash, Consumer callback) implements Consumer { + @Override + public int hashCode() { + return hash; + } + + @Override + public void accept(SessionEvent event) { + callback.accept(event); + } + } + var added = new Handler(15, received::add); + session.on(new Handler(0, event -> session.on(added))); + session.on(new Handler(1, event -> session.on(added))); + + dispatchEvent(createSessionIdleEvent()); + assertTrue(received.isEmpty(), "A new wait must not receive the idle event already being dispatched"); + + var next = createAssistantMessageEvent("HELLO"); + dispatchEvent(next); + assertEquals(List.of(next), received); + } + @Test void testExternalToolCompletedCancelsBlockedHandler() throws Exception { var toolFuture = new CompletableFuture(); @@ -357,7 +387,7 @@ void testHandlerReceivesCorrectEventData() { SessionStartEvent startEvent = createSessionStartEvent(); startEvent.setData(new SessionStartEvent.SessionStartEventData("my-session-123", null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null)); + null, null, null, null, null, null, null, null, null, null, null, null)); dispatchEvent(startEvent); AssistantMessageEvent msgEvent = createAssistantMessageEvent("Test content"); @@ -1034,7 +1064,7 @@ private SessionStartEvent createSessionStartEvent() { private SessionStartEvent createSessionStartEvent(String sessionId) { var event = new SessionStartEvent(); var data = new SessionStartEvent.SessionStartEventData(sessionId, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null, null); event.setData(data); return event; } diff --git a/java/sdk/src/test/java/com/github/copilot/SessionHandlerTest.java b/java/sdk/src/test/java/com/github/copilot/SessionHandlerTest.java index 345fdccff9..001489e41f 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionHandlerTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionHandlerTest.java @@ -6,10 +6,15 @@ import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -22,6 +27,8 @@ import com.github.copilot.rpc.SessionEndHookOutput; import com.github.copilot.rpc.SessionHooks; import com.github.copilot.rpc.SessionStartHookOutput; +import com.github.copilot.rpc.SubagentStartHookOutput; +import com.github.copilot.rpc.SubagentStopHookOutput; import com.github.copilot.rpc.ToolDefinition; import com.github.copilot.rpc.UserInputRequest; import com.github.copilot.rpc.UserInputResponse; @@ -310,6 +317,81 @@ void testHandleHooksInvokeAgentStop() throws Exception { assertEquals("finish the remaining work", output.getReason()); } + @Test + void testHandleHooksInvokeSubagentStart() throws Exception { + var hooks = new SessionHooks().setOnSubagentStart((hookInput, invocation) -> { + assertEquals("handler-test-session", invocation.getSessionId()); + assertEquals("parent-session", hookInput.sessionId()); + assertEquals(1735689600000L, hookInput.timestamp()); + assertEquals("/tmp", hookInput.cwd()); + assertEquals("/tmp/transcript.jsonl", hookInput.transcriptPath()); + assertEquals("explore", hookInput.agentName()); + assertEquals("Explore Agent", hookInput.agentDisplayName()); + assertEquals("Read code", hookInput.agentDescription()); + return CompletableFuture.completedFuture(new SubagentStartHookOutput("Follow the file")); + }); + assertTrue(hooks.hasHooks()); + session.registerHooks(hooks); + + JsonNode input = MAPPER.valueToTree(Map.of("sessionId", "parent-session", "timestamp", 1735689600000L, "cwd", + "/tmp", "transcriptPath", "/tmp/transcript.jsonl", "agentName", "explore", "agentDisplayName", + "Explore Agent", "agentDescription", "Read code")); + + var output = assertInstanceOf(SubagentStartHookOutput.class, + session.handleHooksInvoke("subagentStart", input).get()); + assertEquals("Follow the file", MAPPER.valueToTree(output).get("additionalContext").asText()); + } + + @Test + void testHandleHooksInvokeSubagentStop() throws Exception { + var hooks = new SessionHooks().setOnSubagentStop((hookInput, invocation) -> { + assertEquals("handler-test-session", invocation.getSessionId()); + assertEquals("parent-session", hookInput.sessionId()); + assertEquals(1735689600001L, hookInput.timestamp()); + assertEquals("/tmp", hookInput.cwd()); + assertEquals("/tmp/transcript.jsonl", hookInput.transcriptPath()); + assertEquals("explore", hookInput.agentName()); + assertEquals("explore", hookInput.agentType()); + assertEquals("read-file", hookInput.agentId()); + assertEquals("Explore Agent", hookInput.agentDisplayName()); + assertEquals("Read code", hookInput.agentDescription()); + assertEquals("end_turn", hookInput.stopReason()); + assertEquals("original answer", hookInput.response()); + return CompletableFuture.completedFuture(new SubagentStopHookOutput(null, null, "rewritten answer")); + }); + assertTrue(hooks.hasHooks()); + session.registerHooks(hooks); + + JsonNode input = MAPPER.valueToTree(Map.ofEntries(Map.entry("sessionId", "parent-session"), + Map.entry("timestamp", 1735689600001L), Map.entry("cwd", "/tmp"), + Map.entry("transcriptPath", "/tmp/transcript.jsonl"), Map.entry("agentName", "explore"), + Map.entry("agentType", "explore"), Map.entry("agentId", "read-file"), + Map.entry("agentDisplayName", "Explore Agent"), Map.entry("agentDescription", "Read code"), + Map.entry("stopReason", "end_turn"), Map.entry("response", "original answer"))); + + var output = assertInstanceOf(SubagentStopHookOutput.class, + session.handleHooksInvoke("subagentStop", input).get()); + JsonNode result = MAPPER.valueToTree(output); + assertEquals("rewritten answer", result.get("modifiedResponse").asText()); + assertFalse(result.has("decision")); + assertFalse(result.has("reason")); + } + + @Test + void testHandleHooksInvokeSubagentStopBlock() throws Exception { + session.registerHooks(new SessionHooks().setOnSubagentStop((input, invocation) -> CompletableFuture + .completedFuture(new SubagentStopHookOutput("block", "Keep researching", null)))); + + JsonNode result = MAPPER + .valueToTree(session + .handleHooksInvoke("subagentStop", + MAPPER.valueToTree(Map.of("sessionId", "parent-session", "agentName", "explore"))) + .get()); + assertEquals("block", result.get("decision").asText()); + assertEquals("Keep researching", result.get("reason").asText()); + assertFalse(result.has("modifiedResponse")); + } + // ===== handleHooksInvoke: sessionId deserialization on hook inputs ===== @Test @@ -371,6 +453,44 @@ void testHandleHooksInvokeUnhandledHookType() throws Exception { assertNull(result); } + @Test + void testHandleHooksInvokeSubagentLifecycleDoesNotLogAsUnhandled() throws Exception { + session.registerHooks(new SessionHooks()); + Logger logger = Logger.getLogger(CopilotSession.class.getName()); + Level previousLevel = logger.getLevel(); + List unhandled = new ArrayList<>(); + Handler handler = new Handler() { + @Override + public void publish(LogRecord record) { + if (record.getMessage().startsWith("Unhandled hook type: ")) { + unhandled.add(record.getMessage()); + } + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + }; + handler.setLevel(Level.FINE); + logger.addHandler(handler); + logger.setLevel(Level.FINE); + try { + JsonNode input = MAPPER.valueToTree(Map.of("sessionId", "handler-test-session", "agentName", "task")); + assertNull(session.handleHooksInvoke("subagentStart", input).get()); + assertNull(session.handleHooksInvoke("subagentStop", input).get()); + assertNull(session.handleHooksInvoke("unknownHookType", input).get()); + + assertEquals(List.of("Unhandled hook type: unknownHookType"), unhandled); + } finally { + logger.removeHandler(handler); + logger.setLevel(previousLevel); + } + } + // ===== handleHooksInvoke: handler throws ===== @Test diff --git a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java index bbbaee9c3c..c7cb1be6bc 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionRequestBuilderTest.java @@ -36,6 +36,7 @@ import com.github.copilot.rpc.MemoryConfiguration; import com.github.copilot.rpc.ResumeSessionConfig; import com.github.copilot.rpc.ResumeSessionRequest; +import com.github.copilot.rpc.ResumeSessionResponse; import com.github.copilot.rpc.SessionConfig; import com.github.copilot.rpc.SessionHooks; import com.github.copilot.rpc.ToolDefinition; @@ -49,6 +50,56 @@ */ public class SessionRequestBuilderTest { + @Test + void transcriptRecoveryOverridesAndResponseInAllModes() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + for (var mode : new CopilotClientMode[]{CopilotClientMode.COPILOT_CLI, CopilotClientMode.EMPTY}) { + for (Boolean choice : new Boolean[]{null, false, true}) { + var config = new ResumeSessionConfig(); + if (choice != null) { + config.setAllowTranscriptRecovery(choice); + } + var request = SessionRequestBuilder.buildResumeRequest("s1", config.clone(), mode); + var requestJson = mapper.readTree(mapper.writeValueAsBytes(request)); + if (choice == null) { + assertFalse(requestJson.has("allowTranscriptRecovery")); + } else { + assertTrue(requestJson.has("allowTranscriptRecovery")); + assertEquals(choice.booleanValue(), requestJson.path("allowTranscriptRecovery").asBoolean()); + } + } + } + + var response = mapper + .readValue( + "{\"sessionId\":\"s1\",\"transcriptRecovery\":{\"plannedBackupPath\":\"backup.jsonl\"," + + "\"invalidLineNumbers\":[3,4],\"sessionStartMoved\":true}}", + ResumeSessionResponse.class); + assertNotNull(response.transcriptRecovery()); + assertEquals("backup.jsonl", response.transcriptRecovery().plannedBackupPath()); + assertEquals(List.of(3, 4), response.transcriptRecovery().invalidLineNumbers()); + assertTrue(response.transcriptRecovery().sessionStartMoved()); + assertNull(mapper.readValue("{\"sessionId\":\"s1\"}", ResumeSessionResponse.class).transcriptRecovery()); + assertNull(new ResumeSessionResponse("s1", null, null, null).transcriptRecovery()); + } + + @Test + void clearingTranscriptRecoveryRestoresRuntimeDefaultInAllModes() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + for (boolean choice : new boolean[]{false, true}) { + var config = new ResumeSessionConfig().setAllowTranscriptRecovery(choice); + assertEquals(choice, config.getAllowTranscriptRecovery().orElseThrow()); + assertSame(config, config.clearAllowTranscriptRecovery()); + assertTrue(config.getAllowTranscriptRecovery().isEmpty()); + assertFalse(mapper.readTree(mapper.writeValueAsBytes(config)).has("allowTranscriptRecovery")); + + var cli = SessionRequestBuilder.buildResumeRequest("s1", config.clone(), CopilotClientMode.COPILOT_CLI); + var empty = SessionRequestBuilder.buildResumeRequest("s1", config.clone(), CopilotClientMode.EMPTY); + assertFalse(mapper.readTree(mapper.writeValueAsBytes(cli)).has("allowTranscriptRecovery")); + assertFalse(mapper.readTree(mapper.writeValueAsBytes(empty)).has("allowTranscriptRecovery")); + } + } + // ========================================================================= // buildCreateRequest // ========================================================================= @@ -359,8 +410,9 @@ void continuePendingWorkIsForwardedAndSerializedOnResume() throws Exception { var request = SessionRequestBuilder.buildResumeRequest("sid-pending", config.clone()); assertEquals(enabled, request.getContinuePendingWork()); - assertEquals(enabled, - mapper.readTree(mapper.writeValueAsBytes(request)).path("continuePendingWork").booleanValue()); + var serialized = mapper.readTree(mapper.writeValueAsBytes(request)); + assertTrue(serialized.has("continuePendingWork")); + assertEquals(enabled, serialized.get("continuePendingWork").booleanValue()); } } diff --git a/java/sdk/src/test/java/com/github/copilot/SetToolsIT.java b/java/sdk/src/test/java/com/github/copilot/SetToolsIT.java new file mode 100644 index 0000000000..e68a6b30f0 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/SetToolsIT.java @@ -0,0 +1,243 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.generated.AssistantMessageEvent; +import com.github.copilot.rpc.MessageOptions; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.ResumeSessionConfig; +import com.github.copilot.rpc.SessionConfig; +import com.github.copilot.rpc.ToolDefinition; + +@AllowCopilotExperimental +class SetToolsIT { + + private static final String FRUIT_PROMPT = "Use lookup_fruit to find the fruit for code 42."; + private static final String FRUIT_AND_VEGETABLE_PROMPT = "Use lookup_fruit to find the fruit for code 42 again, and use lookup_vegetable to find the vegetable for code 7."; + private static final String VEGETABLE_PROMPT = "Use lookup_vegetable to find the vegetable for code 7."; + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + @Test + void replacesToolsOnACreatedSession() throws Exception { + ctx.configureForTest("set_tools", "replaces_tools_on_a_created_session"); + + var originalLookups = new CopyOnWriteArrayList(); + var replacementLookups = new CopyOnWriteArrayList(); + var vegetableLookups = new CopyOnWriteArrayList(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession( + new SessionConfig().setTools(List.of(lookupFruit("apple", originalLookups), retiredLookup())) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(30, TimeUnit.SECONDS); + try { + AssistantMessageEvent first = session.sendAndWait(new MessageOptions().setPrompt(FRUIT_PROMPT)).get(90, + TimeUnit.SECONDS); + assertNotNull(first); + assertTrue(first.getData().content().contains("apple"), first.getData().content()); + + session.setTools( + List.of(lookupFruit("dragonfruit", replacementLookups), lookupVegetable(vegetableLookups))) + .get(30, TimeUnit.SECONDS); + + AssistantMessageEvent second = session + .sendAndWait(new MessageOptions().setPrompt(FRUIT_AND_VEGETABLE_PROMPT)) + .get(90, TimeUnit.SECONDS); + assertNotNull(second); + assertTrue(second.getData().content().contains("dragonfruit"), second.getData().content()); + assertTrue(second.getData().content().contains("carrot"), second.getData().content()); + assertEquals(List.of(42), originalLookups); + assertEquals(List.of(42), replacementLookups); + assertEquals(List.of(7), vegetableLookups); + + // Model requests after the replacement offer exactly the new tool set. + List> exchanges = ctx.getExchanges(); + int replacedFrom = indexOfPrompt(exchanges, FRUIT_AND_VEGETABLE_PROMPT); + assertTrue(replacedFrom > 0, "Expected model requests before and after the replacement"); + assertOffered(exchanges.subList(0, replacedFrom), List.of("lookup_fruit", "retired_lookup"), + List.of("lookup_vegetable")); + assertOffered(exchanges.subList(replacedFrom, exchanges.size()), + List.of("lookup_fruit", "lookup_vegetable"), List.of("retired_lookup")); + } finally { + session.close(); + } + } + } + + @Test + void replacesToolsOnAResumedSession() throws Exception { + ctx.configureForTest("set_tools", "replaces_tools_on_a_resumed_session"); + + try (CopilotClient client = ctx.createClient()) { + var createdLookups = new CopyOnWriteArrayList(); + CopilotSession created = client + .createSession(new SessionConfig().setTools(List.of(lookupFruit("apple", createdLookups))) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(30, TimeUnit.SECONDS); + String sessionId = created.getSessionId(); + AssistantMessageEvent first = created.sendAndWait(new MessageOptions().setPrompt(FRUIT_PROMPT)).get(90, + TimeUnit.SECONDS); + assertNotNull(first); + assertTrue(first.getData().content().contains("apple"), first.getData().content()); + assertEquals(List.of(42), createdLookups); + created.close(); + + var fruitLookups = new CopyOnWriteArrayList(); + var vegetableLookups = new CopyOnWriteArrayList(); + CopilotSession resumed = client.resumeSession(sessionId, + new ResumeSessionConfig().setTools(List.of(lookupFruit("apple", fruitLookups))) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(30, TimeUnit.SECONDS); + try { + resumed.setTools(List.of(lookupVegetable(vegetableLookups))).get(30, TimeUnit.SECONDS); + + AssistantMessageEvent answer = resumed.sendAndWait(new MessageOptions().setPrompt(VEGETABLE_PROMPT)) + .get(90, TimeUnit.SECONDS); + assertNotNull(answer); + assertTrue(answer.getData().content().contains("carrot"), answer.getData().content()); + assertEquals(List.of(7), vegetableLookups); + assertEquals(List.of(), fruitLookups); + + List> exchanges = ctx.getExchanges(); + int replacedFrom = indexOfPrompt(exchanges, VEGETABLE_PROMPT); + assertTrue(replacedFrom > 0, "Expected model requests before and after the replacement"); + assertOffered(exchanges.subList(replacedFrom, exchanges.size()), List.of("lookup_vegetable"), + List.of("lookup_fruit")); + } finally { + resumed.close(); + } + } + } + + @Test + void keepsThePreviousToolsWhenAReplacementIsRejected() throws Exception { + ctx.configureForTest("set_tools", "keeps_the_previous_tools_when_a_replacement_is_rejected"); + + var originalLookups = new CopyOnWriteArrayList(); + var replacementLookups = new CopyOnWriteArrayList(); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setTools(List.of(lookupFruit("apple", originalLookups))) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(30, TimeUnit.SECONDS); + try { + assertThrows(ExecutionException.class, + () -> session.setTools(List.of(lookupFruit("dragonfruit", replacementLookups), invalidTool())) + .get(30, TimeUnit.SECONDS)); + + AssistantMessageEvent answer = session.sendAndWait(new MessageOptions().setPrompt(FRUIT_PROMPT)).get(90, + TimeUnit.SECONDS); + assertNotNull(answer); + assertTrue(answer.getData().content().contains("apple"), answer.getData().content()); + assertEquals(List.of(42), originalLookups); + assertEquals(List.of(), replacementLookups); + } finally { + session.close(); + } + } + } + + private static ToolDefinition lookupFruit(String fruit, List calls) { + return ToolDefinition.create("lookup_fruit", "Looks up the fruit for a numeric code", codeSchema("Fruit code"), + invocation -> { + calls.add(((Number) invocation.getArguments().get("code")).intValue()); + return CompletableFuture.completedFuture(fruit); + }); + } + + private static ToolDefinition lookupVegetable(List calls) { + return ToolDefinition.create("lookup_vegetable", "Looks up the vegetable for a numeric code", + codeSchema("Vegetable code"), invocation -> { + calls.add(((Number) invocation.getArguments().get("code")).intValue()); + return CompletableFuture.completedFuture("carrot"); + }); + } + + private static ToolDefinition retiredLookup() { + return ToolDefinition.create("retired_lookup", "Looks up a retired value", + Map.of("type", "object", "properties", Map.of()), + invocation -> CompletableFuture.completedFuture("retired")); + } + + private static ToolDefinition invalidTool() { + return ToolDefinition.create("invalid.tool", "Has a name the runtime rejects", + Map.of("type", "object", "properties", Map.of()), + invocation -> CompletableFuture.completedFuture("never")); + } + + private static Map codeSchema(String description) { + return Map.of("type", "object", "properties", + Map.of("code", Map.of("type", "integer", "description", description)), "required", List.of("code")); + } + + /** + * Returns the index of the first model request that carries {@code prompt} as a + * user message, or -1. + */ + private static int indexOfPrompt(List> exchanges, String prompt) { + for (int i = 0; i < exchanges.size(); i++) { + if (exchanges.get(i).get("request") instanceof Map request + && request.get("messages") instanceof List messages) { + for (Object message : messages) { + if (message instanceof Map fields && "user".equals(fields.get("role")) + && String.valueOf(fields.get("content")).contains(prompt)) { + return i; + } + } + } + } + return -1; + } + + /** + * Asserts that every model request offered the tools in {@code offered} and + * none of the tools in {@code notOffered}. + */ + private static void assertOffered(List> exchanges, List offered, + List notOffered) { + for (Map exchange : exchanges) { + var tools = new ArrayList(); + if (exchange.get("request") instanceof Map request && request.get("tools") instanceof List list) { + for (Object tool : list) { + if (tool instanceof Map fields && fields.get("function") instanceof Map function) { + tools.add(String.valueOf(function.get("name"))); + } + } + } + assertTrue(tools.containsAll(offered), "Offered " + tools + ", expected " + offered); + for (String name : notOffered) { + assertFalse(tools.contains(name), "Offered " + tools + ", expected no " + name); + } + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/SetToolsTest.java b/java/sdk/src/test/java/com/github/copilot/SetToolsTest.java new file mode 100644 index 0000000000..6f4723ce58 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/SetToolsTest.java @@ -0,0 +1,216 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.copilot.generated.ExternalToolRequestedEvent; +import com.github.copilot.rpc.ToolDefinition; +import com.github.copilot.rpc.ToolDefer; + +@AllowCopilotExperimental +class SetToolsTest { + + private JsonRpcClient rpc; + private CopilotSession session; + + @BeforeEach + void setup() { + rpc = mock(JsonRpcClient.class); + session = new CopilotSession("set-tools-session", rpc); + } + + @Test + void sendsCompleteReplacementPayloadAndInstallsOnlyHandlersAfterAcceptance() throws Exception { + var accepted = new CompletableFuture(); + List requests = captureSetToolsRequests(accepted); + var originalCalls = new CopyOnWriteArrayList(); + var replacementCalls = new CopyOnWriteArrayList(); + session.registerTools(List.of(tool("original_tool", "Original", "old", originalCalls))); + + ToolDefinition declarationOnly = new ToolDefinition("declaration_only", null, Map.of("type", "object"), null, + true, true, ToolDefer.NEVER, Map.of("owner", "test"), true); + var pending = session + .setTools(List.of(tool("replacement_tool", null, "new", replacementCalls), declarationOnly)); + + assertEquals(1, requests.size()); + JsonNode payload = requests.get(0); + assertEquals("set-tools-session", payload.path("sessionId").asText()); + assertEquals("replacement_tool", payload.at("/tools/0/name").asText()); + assertEquals("", payload.at("/tools/0/description").asText()); + assertEquals("object", payload.at("/tools/0/parameters/type").asText()); + assertFalse(payload.at("/tools/0").has("handler")); + assertEquals("declaration_only", payload.at("/tools/1/name").asText()); + assertEquals("", payload.at("/tools/1/description").asText()); + assertTrue(payload.at("/tools/1/overridesBuiltInTool").asBoolean()); + assertTrue(payload.at("/tools/1/skipPermission").asBoolean()); + assertEquals("never", payload.at("/tools/1/defer").asText()); + assertTrue(payload.at("/tools/1/isTerminal").asBoolean()); + assertEquals("test", payload.at("/tools/1/metadata/owner").asText()); + + dispatchToolRequest("request-old", "original_tool"); + waitForCalls(originalCalls, List.of("original_tool")); + assertNull(session.getTool("replacement_tool")); + + accepted.complete(null); + pending.get(1, TimeUnit.SECONDS); + + assertNull(session.getTool("original_tool")); + dispatchToolRequest("request-new", "replacement_tool"); + waitForCalls(replacementCalls, List.of("replacement_tool")); + } + + @Test + void rejectionLeavesExistingHandlersUnchanged() throws Exception { + var rejected = new CompletableFuture(); + captureSetToolsRequests(rejected); + var originalCalls = new CopyOnWriteArrayList(); + var replacementCalls = new CopyOnWriteArrayList(); + session.registerTools(List.of(tool("lookup", "Original", "old", originalCalls))); + + var pending = session.setTools(List.of(tool("lookup", "Replacement", "new", replacementCalls))); + rejected.completeExceptionally(new IllegalArgumentException("invalid params")); + + var error = assertThrows(ExecutionException.class, () -> pending.get(1, TimeUnit.SECONDS)); + assertInstanceOf(IllegalArgumentException.class, error.getCause()); + dispatchToolRequest("request-after-reject", "lookup"); + waitForCalls(originalCalls, List.of("lookup")); + assertTrue(replacementCalls.isEmpty()); + } + + @Test + void emptyListRemovesHandlersAfterAcceptance() throws Exception { + var accepted = new CompletableFuture(); + List requests = captureSetToolsRequests(accepted); + session.registerTools(List.of(tool("lookup", "Original", "old", new CopyOnWriteArrayList<>()))); + + var pending = session.setTools(List.of()); + assertTrue(requests.get(0).path("tools").isArray()); + assertEquals(0, requests.get(0).path("tools").size()); + + accepted.complete(null); + pending.get(1, TimeUnit.SECONDS); + assertNull(session.getTool("lookup")); + } + + @Test + void concurrentCallsAreSentInOrderAndFailureDoesNotBlockLaterCall() throws Exception { + var rpcResults = List.of(new CompletableFuture(), new CompletableFuture()); + var nextResult = new AtomicInteger(); + var requests = new ArrayList(); + when(rpc.invoke(eq("session.tools.set"), any(), eq(Void.class))).thenAnswer(invocation -> { + requests.add(invocation.getArgument(1)); + return rpcResults.get(nextResult.getAndIncrement()); + }); + + var first = session.setTools(List.of(tool("first", "First", "first", new CopyOnWriteArrayList<>()))); + var second = session.setTools(List.of(tool("second", "Second", "second", new CopyOnWriteArrayList<>()))); + + assertEquals(1, requests.size(), "second replacement must wait for the first RPC to finish"); + assertEquals("first", requests.get(0).at("/tools/0/name").asText()); + + rpcResults.get(0).completeExceptionally(new IllegalArgumentException("rejected")); + var firstError = assertThrows(ExecutionException.class, () -> first.get(1, TimeUnit.SECONDS)); + assertInstanceOf(IllegalArgumentException.class, firstError.getCause()); + assertEquals(2, requests.size()); + assertEquals("second", requests.get(1).at("/tools/0/name").asText()); + + rpcResults.get(1).complete(null); + second.get(1, TimeUnit.SECONDS); + assertNull(session.getTool("first")); + assertNotNull(session.getTool("second")); + } + + @Test + void cancellingReturnedFutureDoesNotPreventAcceptedReplacementInstallingHandlers() throws Exception { + var accepted = new CompletableFuture(); + captureSetToolsRequests(accepted); + + var pending = session + .setTools(List.of(tool("replacement", "Replacement", "new", new CopyOnWriteArrayList<>()))); + assertTrue(pending.cancel(true)); + + accepted.complete(null); + + assertTrue(pending.isCancelled()); + assertNotNull(session.getTool("replacement")); + } + + @Test + void cancellingWhileQueuedSendsNothing() throws Exception { + var firstResult = new CompletableFuture(); + var requests = captureSetToolsRequests(firstResult); + + var first = session.setTools(List.of(tool("first", "First", "first", new CopyOnWriteArrayList<>()))); + var queued = session.setTools(List.of(tool("queued", "Queued", "queued", new CopyOnWriteArrayList<>()))); + assertTrue(queued.cancel(true)); + + firstResult.complete(null); + first.get(1, TimeUnit.SECONDS); + + assertEquals(1, requests.size()); + assertEquals("first", requests.get(0).at("/tools/0/name").asText()); + assertNotNull(session.getTool("first")); + assertNull(session.getTool("queued")); + } + + private List captureSetToolsRequests(CompletableFuture result) { + var requests = new ArrayList(); + when(rpc.invoke(eq("session.tools.set"), any(), eq(Void.class))).thenAnswer(invocation -> { + requests.add(invocation.getArgument(1)); + return result; + }); + return requests; + } + + private static ToolDefinition tool(String name, String description, String result, List calls) { + return ToolDefinition.create(name, description, Map.of("type", "object"), invocation -> { + calls.add(name); + return CompletableFuture.completedFuture(result); + }); + } + + private void dispatchToolRequest(String requestId, String toolName) { + var requested = new ExternalToolRequestedEvent(); + requested.setData(new ExternalToolRequestedEvent.ExternalToolRequestedEventData(requestId, "set-tools-session", + "tool-call-" + requestId, toolName, null, Map.of(), null, null, null)); + try { + Method dispatchMethod = CopilotSession.class.getDeclaredMethod("dispatchEvent", + com.github.copilot.generated.SessionEvent.class); + dispatchMethod.setAccessible(true); + dispatchMethod.invoke(session, requested); + } catch (Exception e) { + throw new RuntimeException("Failed to dispatch tool request", e); + } + } + + private static void waitForCalls(List calls, List expected) throws Exception { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1); + while (System.nanoTime() < deadline) { + if (calls.equals(expected)) { + return; + } + Thread.onSpinWait(); + } + assertEquals(expected, calls); + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/StreamingFidelityTest.java b/java/sdk/src/test/java/com/github/copilot/StreamingFidelityTest.java index 3701cf9c13..70fe06272d 100644 --- a/java/sdk/src/test/java/com/github/copilot/StreamingFidelityTest.java +++ b/java/sdk/src/test/java/com/github/copilot/StreamingFidelityTest.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterAll; @@ -158,7 +159,7 @@ void testShouldProduceDeltasAfterSessionResume() throws Exception { CopilotSession session2 = newClient.resumeSession(sessionId, new ResumeSessionConfig() .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(true)).get(); - List events = new ArrayList<>(); + List events = new CopyOnWriteArrayList<>(); session2.on(events::add); AssistantMessageEvent answer = session2 @@ -212,7 +213,7 @@ void testShouldNotProduceDeltasAfterSessionResumeWithStreamingDisabled() throws CopilotSession session2 = newClient.resumeSession(sessionId, new ResumeSessionConfig() .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(false)).get(); - List events = new ArrayList<>(); + List events = new CopyOnWriteArrayList<>(); session2.on(events::add); AssistantMessageEvent answer = session2 diff --git a/java/sdk/src/test/java/com/github/copilot/StringSchemaToolsIT.java b/java/sdk/src/test/java/com/github/copilot/StringSchemaToolsIT.java new file mode 100644 index 0000000000..3549b6b3d6 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/StringSchemaToolsIT.java @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import com.github.copilot.rpc.MessageOptions; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.SessionConfig; +import com.github.copilot.rpc.ToolDefinition; + +class StringSchemaToolsIT { + @Test + void stringSchemaApplyPatchOverrideBindsPatchInput() throws Exception { + try (E2ETestContext ctx = E2ETestContext.create()) { + ctx.configureForTest("tools", "string_schema_apply_patch_override_binds_patch_input"); + var receivedPatch = new CompletableFuture(); + var tool = ToolDefinition.create("apply_patch", "Apply a patch", Map.of("type", "string"), invocation -> { + receivedPatch.complete(invocation.getArgumentsAs(String.class)); + return CompletableFuture.completedFuture("HOST_PATCH_HANDLED"); + }).overridesBuiltInTool(true); + try (CopilotClient client = ctx.createClient(); + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setTools(List.of(tool))) + .get(30, TimeUnit.SECONDS)) { + var response = session + .sendAndWait(new MessageOptions().setPrompt("Use apply_patch to apply the supplied patch.")) + .get(60, TimeUnit.SECONDS); + + assertTrue(receivedPatch.isDone(), "Typed string handler was not invoked"); + assertEquals("*** Begin Patch\n*** Add File: override-marker.txt\n+from-native\n*** End Patch", + receivedPatch.get(10, TimeUnit.SECONDS)); + assertNotNull(response); + assertEquals("Host override completed.", response.getData().content()); + assertFalse(Files.exists(ctx.getWorkDir().resolve("override-marker.txt"))); + } + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/SubagentHooksE2ETest.java b/java/sdk/src/test/java/com/github/copilot/SubagentHooksE2ETest.java index c2ad45ff24..536a33cb95 100644 --- a/java/sdk/src/test/java/com/github/copilot/SubagentHooksE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/SubagentHooksE2ETest.java @@ -4,22 +4,30 @@ package com.github.copilot; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.InputStream; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashMap; import java.util.List; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.generated.AssistantMessageEvent; import com.github.copilot.rpc.CopilotClientOptions; import com.github.copilot.rpc.MessageOptions; import com.github.copilot.rpc.PermissionHandler; @@ -27,62 +35,178 @@ import com.github.copilot.rpc.PreToolUseHookOutput; import com.github.copilot.rpc.SessionConfig; import com.github.copilot.rpc.SessionHooks; +import com.github.copilot.rpc.SubagentStartHookInput; +import com.github.copilot.rpc.SubagentStartHookOutput; +import com.github.copilot.rpc.SubagentStopHookInput; +import com.github.copilot.rpc.SubagentStopHookOutput; public class SubagentHooksE2ETest { - private static final String SNAPSHOT_NAME = "should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls"; + private static final String SNAPSHOT_NAME = "should_apply_subagent_lifecycle_hook_outputs"; + private static final String CHILD_CONTEXT = "Subagent start hook verified: read the requested file."; + private static final String STOP_RESPONSE_PREFIX = "Subagent stop hook verified: "; @Test - void shouldInvokePreToolUseAndPostToolUseHooksForSubAgentToolCalls() throws Exception { + void shouldApplySubagentLifecycleHookOutputs() throws Exception { try (E2ETestContext ctx = E2ETestContext.create()) { ctx.configureForTest("subagent_hooks", SNAPSHOT_NAME); ConcurrentLinkedQueue hookLog = new ConcurrentLinkedQueue<>(); + ConcurrentLinkedQueue subagentStarts = new ConcurrentLinkedQueue<>(); + ConcurrentLinkedQueue subagentStops = new ConcurrentLinkedQueue<>(); + ConcurrentLinkedQueue startInvocationIds = new ConcurrentLinkedQueue<>(); + ConcurrentLinkedQueue stopInvocationIds = new ConcurrentLinkedQueue<>(); + CompletableFuture subagentStopped = new CompletableFuture<>(); RecordingForwardingRequestHandler requestHandler = new RecordingForwardingRequestHandler(); + String waitingText = "I've launched an explore agent to read subagent-test.txt. " + + "Waiting for it to complete..."; + String finalText = "The explore agent successfully read the file. " + + "The contents of **subagent-test.txt** are:\n\n```\nHello from subagent test!\n```"; + String parentSessionId = UUID.randomUUID().toString(); + CompletableFuture parentWaiting = new CompletableFuture<>(); HashMap env = new HashMap<>(ctx.getEnvironment()); env.put("COPILOT_EXP_COPILOT_CLI_SESSION_BASED_SUBAGENTS", "true"); try (CopilotClient client = ctx .createClient(new CopilotClientOptions().setEnvironment(env).setRequestHandler(requestHandler))) { - CopilotSession session = client - .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> { - hookLog.add(new HookEntry("pre", input.getToolName(), input.getSessionId())); - return CompletableFuture.completedFuture(PreToolUseHookOutput.allow()); - }).setOnPostToolUse((input, invocation) -> { - hookLog.add(new HookEntry("post", input.getToolName(), input.getSessionId())); - return CompletableFuture.completedFuture((PostToolUseHookOutput) null); - }))) - .get(); - try { - Files.writeString(ctx.getWorkDir().resolve("subagent-test.txt"), "Hello from subagent test!"); - session.sendAndWait(new MessageOptions() - .setPrompt("Use the task tool to spawn an explore agent that reads the file " - + "subagent-test.txt in the current directory and reports its contents. " - + "You must use the task tool.")) - .get(120, TimeUnit.SECONDS); - - HookEntry taskPre = hookLog.stream() - .filter(h -> h.kind().equals("pre") && h.toolName().equals("task")).findFirst() - .orElse(null); - assertNotNull(taskPre, "preToolUse should fire for the parent's 'task' tool call"); - - List viewPre = hookLog.stream() - .filter(h -> h.kind().equals("pre") && h.toolName().equals("view")).toList(); - List viewPost = hookLog.stream() - .filter(h -> h.kind().equals("post") && h.toolName().equals("view")).toList(); - assertFalse(viewPre.isEmpty(), "preToolUse should fire for the sub-agent's 'view' tool call"); - assertFalse(viewPost.isEmpty(), "postToolUse should fire for the sub-agent's 'view' tool call"); - assertNotEquals(taskPre.sessionId(), viewPre.get(0).sessionId(), - "Sub-agent tool hooks should have a different sessionId than parent tool hooks"); - assertSubagentRequestMetadata(requestHandler.inferenceRequests()); - } finally { - session.close(); + CopilotSession session = client.createSession(new SessionConfig().setSessionId(parentSessionId) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setHooks(new SessionHooks().setOnPreToolUse((input, invocation) -> { + hookLog.add(new HookEntry("pre", input.getToolName(), input.getSessionId())); + return CompletableFuture.completedFuture(PreToolUseHookOutput.allow()); + }).setOnPostToolUse((input, invocation) -> { + hookLog.add(new HookEntry("post", input.getToolName(), input.getSessionId())); + // A fast child can inject its result before the fixture's waiting reply is + // requested. + if ("view".equals(input.getToolName()) && !parentSessionId.equals(input.getSessionId())) { + return parentWaiting.thenApply(ignored -> null); + } + return CompletableFuture.completedFuture((PostToolUseHookOutput) null); + }).setOnSubagentStart((input, invocation) -> { + subagentStarts.add(input); + startInvocationIds.add(invocation.getSessionId()); + return CompletableFuture.completedFuture(new SubagentStartHookOutput(CHILD_CONTEXT)); + }).setOnSubagentStop((input, invocation) -> { + subagentStops.add(input); + stopInvocationIds.add(invocation.getSessionId()); + subagentStopped.complete(input); + return CompletableFuture.completedFuture( + new SubagentStopHookOutput(null, null, STOP_RESPONSE_PREFIX + input.response())); + }))).get(); + try (var subscription = session.on(AssistantMessageEvent.class, message -> { + if ((message.getAgentId() == null || message.getAgentId().isEmpty()) + && waitingText.equals(message.getData().content())) { + parentWaiting.complete(null); + } + })) { + try { + Files.writeString(ctx.getWorkDir().resolve("subagent-test.txt"), "Hello from subagent test!"); + var response = session.sendAndWait(new MessageOptions() + .setPrompt("Use the task tool to spawn an explore agent that reads the file " + + "subagent-test.txt in the current directory and reports its contents. " + + "You must use the task tool.")) + .get(120, TimeUnit.SECONDS); + assertNotNull(response); + assertTrue(response.getAgentId() == null || response.getAgentId().isEmpty()); + assertEquals(finalText, response.getData().content()); + var replies = session.getMessages().get().stream() + .filter(event -> event instanceof AssistantMessageEvent + && (event.getAgentId() == null || event.getAgentId().isEmpty())) + .map(event -> ((AssistantMessageEvent) event).getData().content()) + .filter(content -> waitingText.equals(content) || finalText.equals(content)).toList(); + assertEquals(List.of(waitingText, finalText), replies, + "Durable history must contain the waiting reply before the final reply"); + subagentStopped.get(120, TimeUnit.SECONDS); + + HookEntry taskPre = hookLog.stream() + .filter(h -> h.kind().equals("pre") && h.toolName().equals("task")).findFirst() + .orElse(null); + assertNotNull(taskPre, "preToolUse should fire for the parent's 'task' tool call"); + + List viewPre = hookLog.stream() + .filter(h -> h.kind().equals("pre") && h.toolName().equals("view")).toList(); + List viewPost = hookLog.stream() + .filter(h -> h.kind().equals("post") && h.toolName().equals("view")).toList(); + assertFalse(viewPre.isEmpty(), "preToolUse should fire for the sub-agent's 'view' tool call"); + assertFalse(viewPost.isEmpty(), "postToolUse should fire for the sub-agent's 'view' tool call"); + assertNotEquals(taskPre.sessionId(), viewPre.get(0).sessionId(), + "Sub-agent tool hooks should have a different sessionId than parent tool hooks"); + assertSubagentRequestMetadata(requestHandler.inferenceRequests()); + ObjectMapper mapper = new ObjectMapper(); + boolean childContextObserved = false; + for (RequestRecord request : requestHandler.inferenceRequests()) { + if (request.parentAgentId() == null || request.parentAgentId().isEmpty()) { + continue; + } + for (JsonNode message : mapper.readTree(request.body()).path("messages")) { + if (hasChildContext(message)) { + childContextObserved = true; + } + } + } + assertTrue(childContextObserved, + "start hook context should be prepended to the child inference prompt"); + assertTrue( + requestHandler.inferenceRequests().stream() + .anyMatch(r -> (r.parentAgentId() == null || r.parentAgentId().isEmpty()) + && r.body().contains(STOP_RESPONSE_PREFIX)), + "rewritten stop response should reach a parent inference request"); + + assertEquals(1, subagentStarts.size(), "one start hook per launched subagent"); + assertEquals(List.of(session.getSessionId()), List.copyOf(startInvocationIds)); + SubagentStartHookInput start = subagentStarts.element(); + assertEquals(session.getSessionId(), start.sessionId()); + assertTrue(start.timestamp() > 0); + assertEquals(ctx.getWorkDir().toAbsolutePath().normalize(), + Path.of(start.cwd()).toAbsolutePath().normalize()); + assertNotNull(start.transcriptPath()); + assertEquals("explore", start.agentName()); + assertNull(start.agentDisplayName()); + assertNull(start.agentDescription()); + + assertEquals(1, subagentStops.size(), "one stop hook per completed subagent"); + assertEquals(List.of(session.getSessionId()), List.copyOf(stopInvocationIds)); + SubagentStopHookInput stop = subagentStops.element(); + assertEquals(start.sessionId(), stop.sessionId()); + assertTrue(stop.timestamp() >= start.timestamp()); + assertEquals(start.cwd(), stop.cwd()); + assertEquals(start.transcriptPath(), stop.transcriptPath()); + assertEquals(start.agentName(), stop.agentName()); + assertEquals(start.agentDisplayName(), stop.agentDisplayName()); + assertEquals(start.agentDescription(), stop.agentDescription()); + assertEquals("explore", stop.agentType()); + assertNotNull(stop.agentId()); + assertFalse(stop.agentId().isBlank()); + assertEquals("end_turn", stop.stopReason()); + assertTrue(stop.response().contains("Hello from subagent test!")); + } finally { + parentWaiting.complete(null); + session.close(); + } } } } } + private static boolean hasChildContext(JsonNode message) { + if (!"user".equals(message.path("role").asText())) { + return false; + } + String expected = CHILD_CONTEXT + "\n\nRead the file \"subagent-test.txt\""; + JsonNode content = message.path("content"); + if (content.isTextual()) { + return content.asText().contains(expected); + } + if (content.isArray()) { + for (JsonNode part : content) { + if ("text".equals(part.path("type").asText()) && part.path("text").asText().contains(expected)) { + return true; + } + } + } + return false; + } + private static void assertSubagentRequestMetadata(List records) { assertFalse(records.isEmpty(), "request handler should observe inference requests"); RequestRecord subagentRequest = records.stream() @@ -104,7 +228,8 @@ private static boolean isInferenceUrl(String url) { private record HookEntry(String kind, String toolName, String sessionId) { } - private record RequestRecord(String url, String agentId, String parentAgentId, String interactionType) { + private record RequestRecord(String url, String agentId, String parentAgentId, String interactionType, + String body) { } private static final class RecordingForwardingRequestHandler extends CopilotRequestHandler { @@ -118,7 +243,10 @@ List inferenceRequests() { protected HttpResponse sendRequest(HttpRequest request, CopilotRequestContext ctx) throws Exception { records.add(new RequestRecord(request.uri().toString(), ctx.agentId(), ctx.parentAgentId(), - ctx.interactionType())); + ctx.interactionType(), + isInferenceUrl(request.uri().toString()) + ? CopilotRequestTestSupport.requestBodyText(request) + : "")); return super.sendRequest(request, ctx); } } diff --git a/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java b/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java deleted file mode 100644 index fd3f92100d..0000000000 --- a/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java +++ /dev/null @@ -1,144 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -package com.github.copilot.e2e; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.concurrent.TimeUnit; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import com.github.copilot.AllowCopilotExperimental; -import com.github.copilot.CopilotClient; -import com.github.copilot.CopilotSession; -import com.github.copilot.E2ETestContext; -import com.github.copilot.generated.AssistantMessageEvent; -import com.github.copilot.generated.rpc.HistoryRewindMode; -import com.github.copilot.generated.rpc.HistoryRewindOutcome; -import com.github.copilot.generated.rpc.SessionHistoryListRewindPointsResult; -import com.github.copilot.generated.rpc.SessionHistoryPreviewRewindParams; -import com.github.copilot.generated.rpc.SessionHistoryRewindParams; -import com.github.copilot.rpc.MessageOptions; -import com.github.copilot.rpc.PermissionHandler; -import com.github.copilot.rpc.SessionConfig; - -@AllowCopilotExperimental -class RewindIT { - - private static final String FILE_NAME = "rewind-sdk.txt"; - private static final String ORIGINAL_FILE_CONTENT = "Original rewind content"; - private static final String PREPARED_FILE_CONTENT = "Prepared rewind content"; - private static final String FILE_CONTENT = "SDK rewind content"; - - private static E2ETestContext ctx; - - @BeforeAll - static void setup() throws Exception { - ctx = E2ETestContext.create(); - } - - @AfterAll - static void teardown() throws Exception { - if (ctx != null) { - ctx.close(); - } - } - - @Test - void shouldRestoreTrackedFileAndConversation() throws Exception { - ctx.configureForTest("rewind", "should_restore_tracked_file_and_conversation"); - Path filePath = ctx.getWorkDir().resolve(FILE_NAME); - Files.writeString(filePath, ORIGINAL_FILE_CONTENT); - - try (CopilotClient client = ctx.createClient(); - CopilotSession session = client.createSession(new SessionConfig().setModel("claude-sonnet-5") - .setEnableFileChangeTracking(true).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) - .get(30, TimeUnit.SECONDS)) { - AssistantMessageEvent ready = session - .sendAndWait(new MessageOptions().setPrompt("Use the edit tool to replace the exact contents of " - + FILE_NAME + " from " + ORIGINAL_FILE_CONTENT + " to " + PREPARED_FILE_CONTENT - + ". After the tool succeeds, reply with exactly SDK_REWIND_READY."), 30_000) - .get(60, TimeUnit.SECONDS); - assertNotNull(ready); - assertEquals("SDK_REWIND_READY", ready.getData().content()); - assertEquals(PREPARED_FILE_CONTENT, Files.readString(filePath)); - - AssistantMessageEvent response = session - .sendAndWait(new MessageOptions().setPrompt("Use the edit tool to replace the exact contents of " - + FILE_NAME + " from " + PREPARED_FILE_CONTENT + " to " + FILE_CONTENT - + ". After the tool succeeds, reply with exactly SDK_REWIND_DONE."), 30_000) - .get(60, TimeUnit.SECONDS); - - assertNotNull(response); - assertEquals("SDK_REWIND_DONE", response.getData().content()); - assertEquals(FILE_CONTENT, Files.readString(filePath)); - - SessionHistoryListRewindPointsResult rewindPoints = waitForRewindPoints(session); - assertTrue(Boolean.TRUE.equals(rewindPoints.fileChangeTrackingEnabled())); - assertEquals(2, rewindPoints.points().size()); - var rewindPoint = rewindPoints.points().get(1); - assertTrue(Boolean.TRUE.equals(rewindPoint.turnChangedFiles())); - assertTrue(Boolean.TRUE.equals(rewindPoint.canRestoreFiles())); - assertEquals(1L, rewindPoint.fileCount()); - - var preview = session.getRpc().history - .previewRewind(new SessionHistoryPreviewRewindParams(null, rewindPoint.eventId())) - .get(10, TimeUnit.SECONDS); - assertTrue(Boolean.TRUE.equals(preview.available())); - assertEquals(1, preview.files().size()); - assertSamePath(filePath, preview.files().get(0).path()); - - var rewind = session.getRpc().history.rewind(new SessionHistoryRewindParams(null, rewindPoint.eventId(), - HistoryRewindMode.CONVERSATION_AND_FILES)).get(10, TimeUnit.SECONDS); - assertEquals(HistoryRewindOutcome.SUCCESS, rewind.outcome()); - assertTrue(rewind.eventsRemoved() != null && rewind.eventsRemoved() > 0); - assertEquals(1, rewind.restoredFiles().size()); - assertSamePath(filePath, rewind.restoredFiles().get(0)); - assertEquals(PREPARED_FILE_CONTENT, Files.readString(filePath)); - - var events = session.getMessages().get(10, TimeUnit.SECONDS); - assertTrue(events.stream().noneMatch(event -> event.getId().toString().equals(rewindPoint.eventId()))); - } - } - - private static SessionHistoryListRewindPointsResult waitForRewindPoints(CopilotSession session) throws Exception { - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(30); - SessionHistoryListRewindPointsResult result; - do { - result = session.getRpc().history.listRewindPoints().get(10, TimeUnit.SECONDS); - if (result.unavailableReason() == null && result.points().size() == 2 - && Boolean.TRUE.equals(result.points().get(1).turnChangedFiles()) - && Boolean.TRUE.equals(result.points().get(1).canRestoreFiles())) { - return result; - } - TimeUnit.MILLISECONDS.sleep(100); - } while (System.nanoTime() < deadline); - - assertNull(result.unavailableReason(), "Timed out waiting for rewind points to become available"); - assertTrue(result.points().size() >= 2, "Timed out waiting for both rewind points"); - assertTrue(Boolean.TRUE.equals(result.points().get(1).turnChangedFiles()), - "Timed out waiting for the edit turn to capture file changes"); - assertTrue(Boolean.TRUE.equals(result.points().get(1).canRestoreFiles()), - "Timed out waiting for rewind file restoration to become available"); - return result; - } - - private static void assertSamePath(Path expected, String actual) { - String expectedPath = expected.toAbsolutePath().normalize().toString(); - String actualPath = Path.of(actual).toAbsolutePath().normalize().toString(); - if (System.getProperty("os.name").startsWith("Windows")) { - assertTrue(expectedPath.equalsIgnoreCase(actualPath)); - } else { - assertEquals(expectedPath, actualPath); - } - } -} diff --git a/java/sdk/src/test/java/com/github/copilot/generated/GeneratedTypesJacksonRoundTripTest.java b/java/sdk/src/test/java/com/github/copilot/generated/GeneratedTypesJacksonRoundTripTest.java index 1b9b99bb57..f1d40ba04d 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/GeneratedTypesJacksonRoundTripTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/GeneratedTypesJacksonRoundTripTest.java @@ -16,6 +16,7 @@ import java.util.List; import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -53,6 +54,21 @@ private static ObjectMapper createMapper() { return mapper; } + @Test + void futureFileEditKindDoesNotRejectToolCompletion() throws IOException { + var event = MAPPER.readValue( + "{\"type\":\"tool.execution_complete\",\"data\":{\"toolCallId\":\"call-1\",\"success\":true," + + "\"fileEdits\":[{\"path\":\"/future.txt\",\"kind\":\"append\"}]}}", + ToolExecutionCompleteEvent.class); + + assertEquals("call-1", event.getData().toolCallId()); + assertEquals("/future.txt", event.getData().fileEdits().get(0).path()); + assertEquals("append", event.getData().fileEdits().get(0).kind().getValue()); + assertEquals("append", MAPPER.readTree(MAPPER.writeValueAsString(event)).path("data").path("fileEdits").get(0) + .path("kind").asText()); + assertEquals(ToolExecutionCompleteFileEditKind.CREATE, ToolExecutionCompleteFileEditKind.fromValue("create")); + } + @TestFactory Collection roundTripAllGeneratedRecords() { List tests = new ArrayList<>(); diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/ConnectorSerializationTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/ConnectorSerializationTest.java index 281569b8f1..d9b81e7ade 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/ConnectorSerializationTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/ConnectorSerializationTest.java @@ -6,9 +6,13 @@ import static org.junit.jupiter.api.Assertions.*; +import java.util.List; + import org.junit.jupiter.api.Test; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.copilot.AllowCopilotExperimental; @AllowCopilotExperimental @@ -82,6 +86,95 @@ void connectorGeneratedRecords_preserve_wire_names_and_null_omission() throws Ex assertEquals(params, MAPPER.treeToValue(json, SessionConnectorsContinueConnectionParams.class)); } + @Test + void connectorCapabilities_missing_and_false_flags_remain_unsupported() throws Exception { + var json = (ObjectNode) MAPPER.readTree(""" + { + "apiVersion": 1, + "availability": "enabled", + "consentContinuation": true, + "opaqueAccountSelection": true, + "maxPollAttempts": 5, + "maxPollIntervalMs": 2000, + "maxDeadlineMs": 30000 + } + """); + var legacy = MAPPER.treeToValue(json, SessionConnectorsGetCapabilitiesResult.class); + + assertNull(legacy.sessionAccountSelection()); + assertNull(legacy.targetedReconcile()); + assertFalse(Boolean.TRUE.equals(legacy.sessionAccountSelection())); + assertFalse(Boolean.TRUE.equals(legacy.targetedReconcile())); + assertEquals(json, MAPPER.readTree(MAPPER.writeValueAsString(legacy))); + + json.put("sessionAccountSelection", false); + json.put("targetedReconcile", false); + var unsupported = MAPPER.treeToValue(json, SessionConnectorsGetCapabilitiesResult.class); + + assertEquals(Boolean.FALSE, unsupported.sessionAccountSelection()); + assertEquals(Boolean.FALSE, unsupported.targetedReconcile()); + assertFalse(Boolean.TRUE.equals(unsupported.sessionAccountSelection())); + assertFalse(Boolean.TRUE.equals(unsupported.targetedReconcile())); + assertEquals(json, MAPPER.readTree(MAPPER.writeValueAsString(unsupported))); + } + + @Test + void connectorReconcile_preserves_target_and_omits_absent_optional_fields() throws Exception { + var targeted = new SessionConnectorsReconcileRequest("account-1").setRefreshCatalog(false) + .setForceConnectorName("outlook"); + var json = MAPPER.readTree(MAPPER.writeValueAsString(targeted)); + + assertEquals(MAPPER.readTree(""" + {"accountId":"account-1","refreshCatalog":false,"forceConnectorName":"outlook"} + """), json); + + var legacy = new SessionConnectorsReconcileParams(null, "account-1", null); + var legacyJson = MAPPER.readTree(MAPPER.writeValueAsString(legacy)); + assertEquals(MAPPER.readTree(""" + {"accountId":"account-1"} + """), legacyJson); + assertEquals(legacy, MAPPER.treeToValue(legacyJson, SessionConnectorsReconcileParams.class)); + } + + @Test + void connectorCatalogEntry_preserves_optional_presentation_metadata() throws Exception { + var json = (ObjectNode) MAPPER.readTree(""" + { + "name": "outlook", + "displayName": "Outlook", + "description": "Mail and calendar", + "logo": "https://example.com/outlook.svg", + "tier": "standard", + "releaseTag": "preview", + "status": "not_connected", + "runtimeServerIds": [] + } + """); + var entry = MAPPER.treeToValue(json, ConnectorCatalogEntry.class); + + assertEquals("https://example.com/outlook.svg", entry.logo()); + assertEquals("standard", entry.tier()); + assertEquals("preview", entry.releaseTag()); + assertEquals(json, MAPPER.valueToTree(entry)); + + json.remove(List.of("logo", "tier", "releaseTag")); + var legacy = MAPPER.treeToValue(json, ConnectorCatalogEntry.class); + + assertNull(legacy.logo()); + assertNull(legacy.tier()); + assertNull(legacy.releaseTag()); + assertEquals(json, MAPPER.valueToTree(legacy)); + } + + @Test + void connectorConnectResult_rejects_unknown_continuation_outcomes() { + var failure = assertThrows(InvalidTypeIdException.class, () -> MAPPER.readValue(""" + {"kind":"verification_required","continuationId":"continuation-3"} + """, ConnectorConnectResult.class)); + + assertEquals("verification_required", failure.getTypeId()); + } + private static T roundTrip(String json, Class expectedType) throws Exception { var result = MAPPER.readValue(json, ConnectorConnectResult.class); var typedResult = assertInstanceOf(expectedType, result); diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/CustomizationReloadOutcomeTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/CustomizationReloadOutcomeTest.java new file mode 100644 index 0000000000..e8c583024c --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/CustomizationReloadOutcomeTest.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.generated.rpc; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +class CustomizationReloadOutcomeTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void futureReloadValuesRoundTripWithoutChangingKnownValues() throws Exception { + var wire = MAPPER.readTree(""" + {"status":"newStatus","subsystem":"newSubsystem","detail":"new component"} + """); + var outcome = MAPPER.treeToValue(wire, CustomizationReloadOutcome.class); + + assertEquals("newStatus", outcome.status().getValue()); + assertEquals("newSubsystem", outcome.subsystem().getValue()); + assertEquals("new component", outcome.detail()); + assertEquals(wire, MAPPER.valueToTree(outcome)); + assertEquals(CustomizationReloadStatus.RELOADED, CustomizationReloadStatus.fromValue("reloaded")); + assertEquals(CustomizationReloadSubsystem.SKILLS, CustomizationReloadSubsystem.fromValue("skills")); + assertEquals("\"reloaded\"", MAPPER.writeValueAsString(CustomizationReloadStatus.RELOADED)); + assertEquals("\"skills\"", MAPPER.writeValueAsString(CustomizationReloadSubsystem.SKILLS)); + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java index c738d6673d..c925bb80b0 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java @@ -63,164 +63,184 @@ void newRpcEndpoints_dispatch_expected_method_params_and_result_type(String expe } static Stream newRpcDispatches() { - return Stream.of( - dispatch("catalog.select", CatalogSelectionResult.class, """ - {"contract":{"protocolVersion":3,"requiredCapabilities":["catalog-selection"]}, - "sessionId":"catalog-session","selectionRef":"selection-1","outcome":"selected"} - """, - (server, session) -> server.catalog.select( - new CatalogSelectParams(new CatalogClientContract(3L, List.of("catalog-selection")), - "catalog-session", "selection-1", CatalogSelectionDecision.SELECTED))), - dispatch("session.managedSettings.get", SessionManagedSettingsGetResult.class, """ + return Stream + .of(dispatch("session.mcp.list", SessionMcpListResult.class, """ {"sessionId":"bound-session"} - """, (server, session) -> session.managedSettings.get()), - dispatch("session.instructions.reload", Void.class, """ - {"sessionId":"bound-session"} - """, (server, session) -> session.instructions.reload()), - dispatch("session.customizations.reload", SessionCustomizationsReloadResult.class, """ - {"sessionId":"bound-session"} - """, (server, session) -> session.customizations.reload()), - dispatch("session.plugins.install", SessionPluginsInstallResult.class, """ - {"sessionId":"bound-session","source":"plugin@market"} - """, - (server, session) -> session.plugins - .install(new SessionPluginsInstallParams("foreign-session", "plugin@market"))), - dispatch("session.plugins.uninstall", Void.class, """ - {"sessionId":"bound-session","name":"plugin","directSourceId":"source-1"} - """, - (server, session) -> session.plugins - .uninstall(new SessionPluginsUninstallParams("foreign-session", "plugin", "source-1"))), - dispatch("session.plugins.update", SessionPluginsUpdateResult.class, """ - {"sessionId":"bound-session","name":"plugin@market"} - """, - (server, session) -> session.plugins - .update(new SessionPluginsUpdateParams("foreign-session", "plugin@market"))), - dispatch("session.plugins.enable", Void.class, """ - {"sessionId":"bound-session","names":["first@market","second@market"]} - """, - (server, session) -> session.plugins.enable(new SessionPluginsEnableParams("foreign-session", - List.of("first@market", "second@market")))), - dispatch("session.plugins.disable", Void.class, """ - {"sessionId":"bound-session","names":["plugin@market"]} - """, - (server, session) -> session.plugins - .disable(new SessionPluginsDisableParams("foreign-session", List.of("plugin@market")))), - dispatch("session.plugins.marketplaces.list", SessionPluginsMarketplacesListResult.class, """ - {"sessionId":"bound-session"} - """, (server, session) -> session.plugins.marketplaces.list()), - dispatch("session.plugins.marketplaces.add", SessionPluginsMarketplacesAddResult.class, """ - {"sessionId":"bound-session","source":"./market","workingDirectory":"/workspace"} - """, - (server, session) -> session.plugins.marketplaces.add( + """, (server, session) -> session.mcp.list()), + dispatch("session.mcp.listConfigured", SessionMcpListConfiguredResult.class, """ + {"sessionId":"bound-session"} + """, (server, session) -> session.mcp.listConfigured()), + dispatch("catalog.select", CatalogSelectionResult.class, """ + {"contract":{"protocolVersion":3,"requiredCapabilities":["catalog-selection"]}, + "sessionId":"catalog-session","selectionRef":"selection-1","outcome":"selected"} + """, + (server, session) -> server.catalog.select(new CatalogSelectParams( + new CatalogClientContract(3L, List.of("catalog-selection")), "catalog-session", + "selection-1", CatalogSelectionDecision.SELECTED))), + dispatch("session.managedSettings.get", SessionManagedSettingsGetResult.class, """ + {"sessionId":"bound-session"} + """, (server, session) -> session.managedSettings.get()), + dispatch("session.instructions.reload", SessionInstructionsGetSourcesResult.class, """ + {"sessionId":"bound-session"} + """, (server, session) -> session.instructions.reload()), + dispatch("session.customizations.reload", CustomizationsReloadResult.class, """ + {"sessionId":"bound-session"} + """, (server, session) -> session.customizations.reload()), + dispatch("session.plugins.install", SessionPluginsInstallResult.class, """ + {"sessionId":"bound-session","source":"plugin@market"} + """, + (server, session) -> session.plugins + .install(new SessionPluginsInstallParams("foreign-session", "plugin@market"))), + dispatch("session.plugins.uninstall", Void.class, """ + {"sessionId":"bound-session","name":"plugin","directSourceId":"source-1"} + """, + (server, session) -> session.plugins.uninstall( + new SessionPluginsUninstallParams("foreign-session", "plugin", "source-1"))), + dispatch("session.plugins.update", SessionPluginsUpdateResult.class, """ + {"sessionId":"bound-session","name":"plugin@market"} + """, + (server, session) -> session.plugins + .update(new SessionPluginsUpdateParams("foreign-session", "plugin@market"))), + dispatch("session.plugins.enable", Void.class, """ + {"sessionId":"bound-session","names":["first@market","second@market"]} + """, + (server, session) -> session.plugins.enable(new SessionPluginsEnableParams( + "foreign-session", List.of("first@market", "second@market")))), + dispatch("session.plugins.disable", Void.class, """ + {"sessionId":"bound-session","names":["plugin@market"]} + """, + (server, session) -> session.plugins.disable( + new SessionPluginsDisableParams("foreign-session", List.of("plugin@market")))), + dispatch("session.plugins.marketplaces.list", SessionPluginsMarketplacesListResult.class, """ + {"sessionId":"bound-session"} + """, (server, session) -> session.plugins.marketplaces.list()), + dispatch("session.plugins.marketplaces.add", SessionPluginsMarketplacesAddResult.class, """ + {"sessionId":"bound-session","source":"./market","workingDirectory":"/workspace"} + """, (server, session) -> session.plugins.marketplaces.add( new SessionPluginsMarketplacesAddParams("foreign-session", "./market", "/workspace"))), - dispatch("session.plugins.marketplaces.remove", SessionPluginsMarketplacesRemoveResult.class, """ - {"sessionId":"bound-session","name":"market","force":true} - """, - (server, session) -> session.plugins.marketplaces - .remove(new SessionPluginsMarketplacesRemoveParams("foreign-session", "market", true))), - dispatch("session.plugins.marketplaces.browse", SessionPluginsMarketplacesBrowseResult.class, """ - {"sessionId":"bound-session","name":"market"} - """, - (server, session) -> session.plugins.marketplaces - .browse(new SessionPluginsMarketplacesBrowseParams("foreign-session", "market"))), - dispatch("session.plugins.marketplaces.refresh", SessionPluginsMarketplacesRefreshResult.class, """ - {"sessionId":"bound-session","name":"market"} - """, - (server, session) -> session.plugins.marketplaces - .refresh(new SessionPluginsMarketplacesRefreshParams("foreign-session", "market"))), - dispatch("session.plugins.marketplaces.refresh", SessionPluginsMarketplacesRefreshResult.class, """ - {"sessionId":"bound-session"} - """, (server, session) -> session.plugins.marketplaces.refresh(null)), - dispatch("session.queue.withdrawMessage", SessionQueueWithdrawMessageResult.class, """ - {"sessionId":"bound-session","messageId":"message-1","expectedPrompt":"original"} - """, - (server, session) -> session.queue.withdrawMessage( - new SessionQueueWithdrawMessageParams("foreign-session", "message-1", "original"))), - dispatch("session.queue.appendSteering", SessionQueueAppendSteeringResult.class, """ - {"sessionId":"bound-session","messageId":"message-1","expectedPrompt":"original", - "agentMode":"interactive","prompt":"more","displayPrompt":"preview","attachments":[]} - """, (server, - session) -> session.queue.appendSteering(new SessionQueueAppendSteeringParams("foreign-session", - "message-1", "original", SendAgentMode.INTERACTIVE, "more", "preview", List.of()))), - dispatch("session.workflow.run", SessionWorkflowRunResult.class, """ - {"sessionId":"bound-session","name":"workflow-1","args":{"topic":"input"}} - """, - (server, session) -> session.workflow.run(new SessionWorkflowRunParams("foreign-session", - "workflow-1", Map.of("topic", "input"), null))), - dispatch("session.workflow.resume", SessionWorkflowResumeResult.class, """ - {"sessionId":"bound-session","runId":"run-1","notifyOnComplete":true,"logPhaseNames":false} - """, - (server, session) -> session.workflow.resume( - new SessionWorkflowResumeParams("foreign-session", "run-1", null, true, false))), - dispatch("session.workflow.runFromTool", SessionWorkflowRunFromToolResult.class, """ - {"sessionId":"bound-session","name":"workflow-1","args":{"topic":"input"},"toolCallId":"tool-1"} - """, - (server, session) -> session.workflow.runFromTool(new SessionWorkflowRunFromToolParams( - "foreign-session", "workflow-1", Map.of("topic", "input"), null, "tool-1"))), - dispatch("session.workflow.resumeFromTool", SessionWorkflowResumeFromToolResult.class, """ - {"sessionId":"bound-session","runId":"run-1","toolCallId":"tool-1"} - """, - (server, session) -> session.workflow.resumeFromTool( + dispatch("session.plugins.marketplaces.remove", SessionPluginsMarketplacesRemoveResult.class, + """ + {"sessionId":"bound-session","name":"market","force":true} + """, + (server, session) -> session.plugins.marketplaces.remove( + new SessionPluginsMarketplacesRemoveParams("foreign-session", "market", true))), + dispatch("session.plugins.marketplaces.browse", SessionPluginsMarketplacesBrowseResult.class, + """ + {"sessionId":"bound-session","name":"market"} + """, + (server, session) -> session.plugins.marketplaces.browse( + new SessionPluginsMarketplacesBrowseParams("foreign-session", "market"))), + dispatch("session.plugins.marketplaces.refresh", SessionPluginsMarketplacesRefreshResult.class, + """ + {"sessionId":"bound-session","name":"market"} + """, + (server, session) -> session.plugins.marketplaces.refresh( + new SessionPluginsMarketplacesRefreshParams("foreign-session", "market"))), + dispatch( + "session.plugins.marketplaces.refresh", SessionPluginsMarketplacesRefreshResult.class, + """ + {"sessionId":"bound-session"} + """, (server, session) -> session.plugins.marketplaces.refresh(null)), + dispatch("session.queue.withdrawMessage", SessionQueueWithdrawMessageResult.class, """ + {"sessionId":"bound-session","messageId":"message-1","expectedPrompt":"original"} + """, (server, + session) -> session.queue.withdrawMessage(new SessionQueueWithdrawMessageParams( + "foreign-session", "message-1", "original"))), + dispatch("session.queue.appendSteering", SessionQueueAppendSteeringResult.class, """ + {"sessionId":"bound-session","messageId":"message-1","expectedPrompt":"original", + "agentMode":"interactive","prompt":"more","displayPrompt":"preview","attachments":[]} + """, + (server, session) -> session.queue.appendSteering( + new SessionQueueAppendSteeringParams("foreign-session", "message-1", "original", + SendAgentMode.INTERACTIVE, "more", "preview", List.of()))), + dispatch("session.workflow.run", SessionWorkflowRunResult.class, """ + {"sessionId":"bound-session","name":"workflow-1","args":{"topic":"input"}} + """, + (server, session) -> session.workflow.run(new SessionWorkflowRunParams( + "foreign-session", "workflow-1", Map.of("topic", "input"), null))), + dispatch( + "session.workflow.resume", SessionWorkflowResumeResult.class, + """ + {"sessionId":"bound-session","runId":"run-1","notifyOnComplete":true,"logPhaseNames":false} + """, + (server, session) -> session.workflow.resume(new SessionWorkflowResumeParams( + "foreign-session", "run-1", null, true, false))), + dispatch( + "session.workflow.runFromTool", WorkflowRunResult.class, + """ + {"sessionId":"bound-session","name":"workflow-1","args":{"topic":"input"},"toolCallId":"tool-1"} + """, + (server, session) -> session.workflow.runFromTool(new SessionWorkflowRunFromToolParams( + "foreign-session", "workflow-1", Map.of("topic", "input"), null, "tool-1"))), + dispatch("session.workflow.resumeFromTool", WorkflowResumeResult.class, """ + {"sessionId":"bound-session","runId":"run-1","toolCallId":"tool-1"} + """, (server, session) -> session.workflow.resumeFromTool( new SessionWorkflowResumeFromToolParams("foreign-session", "run-1", null, "tool-1"))), - dispatch("session.workflow.getRun", SessionWorkflowGetRunResult.class, """ - {"sessionId":"bound-session","runId":"run-1"} - """, - (server, session) -> session.workflow - .getRun(new SessionWorkflowGetRunParams("foreign-session", "run-1"))), - dispatch("session.workflow.listRuns", SessionWorkflowListRunsResult.class, """ - {"sessionId":"bound-session","afterSeq":2,"limit":5} - """, - (server, session) -> session.workflow - .listRuns(new SessionWorkflowListRunsParams("foreign-session", 2L, null, 5L))), - dispatch("session.workflow.getRunDetail", SessionWorkflowGetRunDetailResult.class, """ - {"sessionId":"bound-session","runId":"run-1"} - """, - (server, session) -> session.workflow - .getRunDetail(new SessionWorkflowGetRunDetailParams("foreign-session", "run-1"))), - dispatch("session.workflow.getRunProgress", SessionWorkflowGetRunProgressResult.class, """ - {"sessionId":"bound-session","runId":"run-1","phaseId":"phase-1","afterSeq":2,"limit":5} - """, - (server, session) -> session.workflow.getRunProgress(new SessionWorkflowGetRunProgressParams( - "foreign-session", "run-1", "phase-1", 2L, null, 5L))), - dispatch("session.workflow.cancel", SessionWorkflowCancelResult.class, """ - {"sessionId":"bound-session","runId":"run-1"} - """, - (server, session) -> session.workflow - .cancel(new SessionWorkflowCancelParams("foreign-session", "run-1"))), - dispatch("session.workflow.pause", SessionWorkflowPauseResult.class, """ - {"sessionId":"bound-session","runId":"run-1"} - """, - (server, session) -> session.workflow - .pause(new SessionWorkflowPauseParams("foreign-session", "run-1"))), - dispatch("session.workflow.pauseAtCheckpoint", SessionWorkflowPauseAtCheckpointResult.class, - """ - {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"checkpoint-1"} + dispatch("session.workflow.getRun", SessionWorkflowGetRunResult.class, """ + {"sessionId":"bound-session","runId":"run-1"} + """, + (server, session) -> session.workflow + .getRun(new SessionWorkflowGetRunParams("foreign-session", "run-1"))), + dispatch("session.workflow.listRuns", SessionWorkflowListRunsResult.class, """ + {"sessionId":"bound-session","afterSeq":2,"limit":5} + """, + (server, session) -> session.workflow + .listRuns(new SessionWorkflowListRunsParams("foreign-session", 2L, null, 5L))), + dispatch("session.workflow.getRunDetail", SessionWorkflowGetRunDetailResult.class, """ + {"sessionId":"bound-session","runId":"run-1"} """, - (server, session) -> session.workflow - .pauseAtCheckpoint(new SessionWorkflowPauseAtCheckpointParams("foreign-session", - "run-1", "execution-1", "checkpoint-1"))), - dispatch("session.workflow.log", Void.class, """ - {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1", - "lines":[{"seq":1,"kind":"log","text":"progress"}]} - """, (server, - session) -> session.workflow.log(new SessionWorkflowLogParams("foreign-session", "run-1", - "execution-1", List.of(new WorkflowLogLine(1L, WorkflowLogLineKind.LOG, "progress"))))), - dispatch("session.workflow.agent", SessionWorkflowAgentResult.class, - """ - {"sessionId":"bound-session","workflowRunId":"run-1","executionToken":"execution-1","prompt":"inspect"} + (server, session) -> session.workflow.getRunDetail( + new SessionWorkflowGetRunDetailParams("foreign-session", "run-1"))), + dispatch("session.workflow.getRunProgress", SessionWorkflowGetRunProgressResult.class, """ + {"sessionId":"bound-session","runId":"run-1","phaseId":"phase-1","afterSeq":2,"limit":5} + """, (server, + session) -> session.workflow.getRunProgress(new SessionWorkflowGetRunProgressParams( + "foreign-session", "run-1", "phase-1", 2L, null, 5L))), + dispatch("session.workflow.cancel", SessionWorkflowCancelResult.class, """ + {"sessionId":"bound-session","runId":"run-1"} """, - (server, session) -> session.workflow.agent(new SessionWorkflowAgentParams("foreign-session", - "run-1", "execution-1", "inspect", null))), - dispatch("session.workflow.journal.get", SessionWorkflowJournalGetResult.class, """ - {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"journal-1"} - """, - (server, session) -> session.workflow.journal.get(new SessionWorkflowJournalGetParams( - "foreign-session", "run-1", "execution-1", "journal-1"))), - dispatch("session.workflow.journal.put", Void.class, """ - {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"journal-1", - "resultJson":{"answer":42}} - """, (server, session) -> session.workflow.journal.put(new SessionWorkflowJournalPutParams( - "foreign-session", "run-1", "execution-1", "journal-1", Map.of("answer", 42))))); + (server, session) -> session.workflow + .cancel(new SessionWorkflowCancelParams("foreign-session", "run-1"))), + dispatch("session.workflow.pause", SessionWorkflowPauseResult.class, """ + {"sessionId":"bound-session","runId":"run-1"} + """, + (server, session) -> session.workflow + .pause(new SessionWorkflowPauseParams("foreign-session", "run-1"))), + dispatch( + "session.workflow.pauseAtCheckpoint", WorkflowPauseCheckpointResult.class, + """ + {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"checkpoint-1"} + """, + (server, session) -> session.workflow + .pauseAtCheckpoint(new SessionWorkflowPauseAtCheckpointParams("foreign-session", + "run-1", "execution-1", "checkpoint-1"))), + dispatch("session.workflow.log", Void.class, """ + {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1", + "lines":[{"seq":1,"kind":"log","text":"progress"}]} + """, + (server, session) -> session.workflow.log(new SessionWorkflowLogParams( + "foreign-session", "run-1", "execution-1", + List.of(new WorkflowLogLine(1L, WorkflowLogLineKind.LOG, "progress"))))), + dispatch("session.workflow.agent", SessionWorkflowAgentResult.class, + """ + {"sessionId":"bound-session","workflowRunId":"run-1","executionToken":"execution-1","prompt":"inspect"} + """, + (server, session) -> session.workflow.agent(new SessionWorkflowAgentParams( + "foreign-session", "run-1", "execution-1", "inspect", null))), + dispatch("session.workflow.journal.get", SessionWorkflowJournalGetResult.class, + """ + {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"journal-1"} + """, + (server, session) -> session.workflow.journal.get(new SessionWorkflowJournalGetParams( + "foreign-session", "run-1", "execution-1", "journal-1"))), + dispatch("session.workflow.journal.put", Void.class, + """ + {"sessionId":"bound-session","runId":"run-1","executionToken":"execution-1","key":"journal-1", + "resultJson":{"answer":42}} + """, + (server, session) -> session.workflow.journal + .put(new SessionWorkflowJournalPutParams("foreign-session", "run-1", + "execution-1", "journal-1", Map.of("answer", 42))))); } private static Arguments dispatch(String method, Class resultType, String expectedParams, @@ -551,8 +571,9 @@ void sessionRpc_mcp_list_injects_sessionId() { assertEquals(1, stub.calls.size()); assertEquals("session.mcp.list", stub.calls.get(0).method()); - var params = (Map) stub.calls.get(0).params(); - assertEquals("sess-mcp", params.get("sessionId")); + var mapper = new ObjectMapper(); + assertEquals(mapper.valueToTree(Map.of("sessionId", "sess-mcp")), + mapper.valueToTree(stub.calls.get(0).params())); } @Test diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java index 51fbaa865c..b68100cf85 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java @@ -19,6 +19,7 @@ import org.junit.jupiter.params.provider.MethodSource; import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.AllowCopilotExperimental; import com.github.copilot.TestUtil; /** @@ -222,6 +223,52 @@ void sessionFsSetProviderParams_conventions_enum() { assertThrows(IllegalArgumentException.class, () -> SessionFsSetProviderConventions.fromValue("unknown")); } + @Test + void sessionFsSetProviderCapabilities_preserves_legacy_constructor_and_binary_wire_field() throws Exception { + var mapper = new ObjectMapper(); + var legacy = new SessionFsSetProviderCapabilities(true); + var legacyJson = mapper.readTree(mapper.writeValueAsString(legacy)); + assertTrue(legacyJson.get("sqlite").asBoolean()); + assertFalse(legacyJson.has("binary")); + assertEquals(legacy, mapper.treeToValue(legacyJson, SessionFsSetProviderCapabilities.class)); + + var binary = new SessionFsSetProviderCapabilities(false, true); + var binaryJson = mapper.readTree(mapper.writeValueAsString(binary)); + assertFalse(binaryJson.get("sqlite").asBoolean()); + assertTrue(binaryJson.get("binary").asBoolean()); + assertEquals(binary, mapper.treeToValue(binaryJson, SessionFsSetProviderCapabilities.class)); + } + + @Test + @AllowCopilotExperimental + void sessionFsReadFileBytes_preserves_base64_wire_content() throws Exception { + var mapper = new ObjectMapper(); + var params = new SessionFsReadFileBytesParams("sess-15", "/image.png"); + var paramsJson = mapper.readTree(mapper.writeValueAsString(params)); + assertEquals("sess-15", paramsJson.get("sessionId").asText()); + assertEquals("/image.png", paramsJson.get("path").asText()); + assertEquals(params, mapper.treeToValue(paramsJson, SessionFsReadFileBytesParams.class)); + + var result = new SessionFsReadFileBytesResult("AAECAw==", null); + var resultJson = mapper.readTree(mapper.writeValueAsString(result)); + assertEquals("AAECAw==", resultJson.get("content").asText()); + assertFalse(resultJson.has("error")); + assertEquals(result, mapper.treeToValue(resultJson, SessionFsReadFileBytesResult.class)); + } + + @Test + @AllowCopilotExperimental + void sessionFsWriteFileBytes_preserves_base64_wire_content_and_mode() throws Exception { + var mapper = new ObjectMapper(); + var params = new SessionFsWriteFileBytesParams("sess-15", "/image.png", "AP/+AQ==", 384L); + var json = mapper.readTree(mapper.writeValueAsString(params)); + assertEquals("sess-15", json.get("sessionId").asText()); + assertEquals("/image.png", json.get("path").asText()); + assertEquals("AP/+AQ==", json.get("content").asText()); + assertEquals(384L, json.get("mode").asLong()); + assertEquals(params, mapper.treeToValue(json, SessionFsWriteFileBytesParams.class)); + } + @Test void sessionFsStatParams_record() { var params = new SessionFsStatParams("sess-20", "/etc/hosts"); @@ -287,12 +334,6 @@ void sessionMcpEnableParams_record() { assertEquals("mcp-server-2", params.serverName()); } - @Test - void sessionMcpListParams_record() { - var params = new SessionMcpListParams("sess-27"); - assertEquals("sess-27", params.sessionId()); - } - @Test void sessionMcpReloadParams_record() { var params = new SessionMcpReloadParams("sess-28"); @@ -850,8 +891,10 @@ void sessionMcpListResult_status_enum_all_values() { @Test void sessionModelGetCurrentResult_record() { - var result = new SessionModelGetCurrentResult("claude-sonnet-5", null, null, null, null, null); + var result = new SessionModelGetCurrentResult("claude-sonnet-5", "claude-sonnet-4.5", null, null, null, null, + null); assertEquals("claude-sonnet-5", result.modelId()); + assertEquals("claude-sonnet-4.5", result.planBaseModelId()); } @Test @@ -1015,7 +1058,7 @@ void mcpDiscoverResult_nested() { @Test void modelsListResult_nested() { - var supports = new ModelCapabilitiesSupports(true, null, false, null); + var supports = new ModelCapabilitiesSupports(false, true, null, false, null); var limits = new ModelCapabilitiesLimits(100000L, 8192L, 128000L, null); var capabilities = new ModelCapabilities(supports, limits); var policy = new ModelPolicy(ModelPolicyState.ENABLED, null); @@ -1028,6 +1071,7 @@ void modelsListResult_nested() { assertEquals(1, result.models().size()); assertEquals("gpt-5", result.models().get(0).id()); assertEquals("GPT-5", result.models().get(0).name()); + assertFalse(result.models().get(0).capabilities().supports().thinking()); assertTrue(result.models().get(0).capabilities().supports().vision()); assertFalse(result.models().get(0).capabilities().supports().reasoningEffort()); assertEquals(100000L, result.models().get(0).capabilities().limits().maxPromptTokens()); diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/McpPromptsTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/McpPromptsTest.java new file mode 100644 index 0000000000..4c0eb8050f --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/McpPromptsTest.java @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.generated.rpc; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Generated dispatch and serialization supplement TypeScript's real MCP tests. + */ +class McpPromptsTest { + private final ObjectMapper mapper = new ObjectMapper(); + + @Test + void promptResultsPreserveOpaqueContentAndOptionalFields() throws Exception { + Path directory = Path.of("").toAbsolutePath(); + Path fixture = null; + while (directory != null) { + Path candidate = directory.resolve("test/harness/mcp-prompt-fixtures.json"); + if (Files.exists(candidate)) { + fixture = candidate; + break; + } + directory = directory.getParent(); + } + assertNotNull(fixture, "Cannot find shared MCP prompt fixture"); + JsonNode fixtures = mapper.readTree(fixture.toFile()); + for (String page : List.of("firstPage", "secondPage")) { + var result = mapper.treeToValue(fixtures.get(page), SessionMcpPromptsListResult.class); + assertEquals(fixtures.get(page), mapper.valueToTree(result)); + } + var result = mapper.treeToValue(fixtures.get("richPrompt"), SessionMcpPromptsGetResult.class); + assertEquals(fixtures.get("richPrompt"), mapper.valueToTree(result)); + } + + @Test + void promptNamespaceDispatchesBoundSessionAndPreservesOptionalArguments() throws Exception { + record Call(String method, JsonNode params, Class resultType) { + } + List calls = new ArrayList<>(); + RpcCaller caller = new RpcCaller() { + @Override + public CompletableFuture invoke(String method, Object params, Class resultType) { + calls.add(new Call(method, mapper.valueToTree(params), resultType)); + return CompletableFuture.completedFuture(null); + } + }; + var session = new SessionRpc(caller, "bound-session"); + session.mcp.prompts.list(new SessionMcpPromptsListParams("foreign-session", "fixture", null)).join(); + session.mcp.prompts.list(new SessionMcpPromptsListParams("foreign-session", "fixture", "page:2/opaque+cursor=")) + .join(); + session.mcp.prompts.get(new SessionMcpPromptsGetParams("foreign-session", "fixture", "rich", null)).join(); + session.mcp.prompts.get(new SessionMcpPromptsGetParams("foreign-session", "fixture", "rich", Map.of())).join(); + session.mcp.prompts.get(new SessionMcpPromptsGetParams("foreign-session", "fixture", "rich", + Map.of("topic", "日本語", "style", ""))).join(); + + assertEquals(5, calls.size()); + for (int i = 0; i < calls.size(); i++) { + Call call = calls.get(i); + assertEquals("bound-session", call.params().get("sessionId").asText()); + assertEquals("fixture", call.params().get("serverName").asText()); + assertEquals(i < 2 ? "session.mcp.prompts.list" : "session.mcp.prompts.get", call.method()); + assertEquals(i < 2 ? SessionMcpPromptsListResult.class : SessionMcpPromptsGetResult.class, + call.resultType()); + } + assertFalse(calls.get(0).params().has("cursor")); + assertEquals("page:2/opaque+cursor=", calls.get(1).params().get("cursor").asText()); + assertEquals("rich", calls.get(2).params().get("promptName").asText()); + assertFalse(calls.get(2).params().has("arguments")); + assertEquals(mapper.readTree("{}"), calls.get(3).params().get("arguments")); + assertEquals(mapper.readTree("{\"topic\":\"日本語\",\"style\":\"\"}"), calls.get(4).params().get("arguments")); + } +} diff --git a/nodejs/README.md b/nodejs/README.md index de8b9ba97e..1135d7ea08 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -9,8 +9,8 @@ To use the SDK, you'll need: - Node.js ^20.19.0 or >=22.12.0 The SDK uses an optional `@github/copilot-sdk-` package containing the -Copilot CLI runtime for the host platform. These packages are built from -verified `github/copilot-cli` release assets when the SDK is published, so +Copilot CLI runtime for the host platform. These packages contain the verified +runtime artifacts for that SDK release, so starting the SDK performs no runtime download. Set `COPILOT_CLI_PATH` to use an existing installation instead. @@ -20,6 +20,10 @@ replaces both with the CLI version published for that snapshot. `npm run pack:release` builds the main package and all platform packages. Set `COPILOT_CLI_DOWNLOAD_BASE_URL` to use a release mirror while packaging. +Standalone source builds acquire stable/prerelease runtime assets from +`github/copilot-cli` at `v`, and canonical unstable assets from +`github/copilot-sdk` at `runtime-`. Both checksum and package +downloads use that release location; mirror overrides retain the same tag scheme. Release workflows instead set `COPILOT_SDK_RUNTIME_PACKAGE_DIR` to a directory containing validated runtime npm package roots named for all eight platforms. This keeps `COPILOT_CLI_USE_NPM_PACKAGE` false and embeds those runtime files in @@ -48,6 +52,7 @@ import { CopilotClient } from "@github/copilot-sdk"; await using client = new CopilotClient(); await client.start(); const host = await client.startAhpHost({ + localServer: {}, onExit: (exit) => { if (exit.error) console.error(exit.error); }, @@ -76,7 +81,15 @@ Use `dispose()` to stop the listener. `reason: "exited"` reports hosting-task failure, not runtime process death, and `exitCode` is absent. Hosting no longer provides process isolation from the runtime. -The runtime validates listener options and applies their defaults: +Select at least one transport explicitly: `localServer: {}` enables the local +listener, `githubEnvironment: { name: "My app", computeId: "stable-installation-id" }` +registers a Mission Control environment and enables remote WPS connections, and +both enables both transports. GitHub-only hosting opens no local listener; +`host.url` and `host.token` are undefined, while `host.environmentId` identifies +the environment. The application supplies a stable compute ID and configures +transports only at startup. Dispose and recreate the host to change them. + +The runtime validates options inside `localServer` and applies their defaults: - `hostname` defaults to `127.0.0.1`. Set it explicitly to request a non-loopback listener, such as `hostname: "0.0.0.0"`, and restrict network access appropriately. @@ -101,6 +114,7 @@ import { approveAll, CopilotClient, defineTool } from "@github/copilot-sdk"; await using client = new CopilotClient(); await using host = await client.startAhpHost({ + localServer: {}, createSession: ({ config, signal }) => { signal.throwIfAborted(); return client.createSession({ @@ -256,6 +270,19 @@ new CopilotClient(options?: CopilotClientOptions) - `telemetry?: TelemetryConfig` - OpenTelemetry configuration for the runtime process. Providing this object enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. - `onGetTraceContext?: TraceContextProvider` - Advanced: callback for linking your application's own OpenTelemetry spans into the same distributed trace as the runtime's spans. Not needed for normal telemetry collection. See [Telemetry](#telemetry) below. - `sessionFs?: SessionFsConfig` - Custom session filesystem provider. + +For a custom provider to serve images to the `view` tool, declare +`sessionFs.capabilities.binary: true` and implement +`readFileBytes(path): Promise` and +`writeFileBytes(path, content: Uint8Array, mode?: number): Promise` on each session's provider. The +adapter transmits exact bytes as standard base64; text-only providers still +support text operations, but image reads fail rather than falling back to the +runtime machine's filesystem. A missing binary implementation is rejected +when a session is created. + +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. + - `sessionIdleTimeoutSeconds?: number` - Server-wide idle timeout for sessions in seconds. Ignored when connecting via `RuntimeConnection.forUri`. - `enableRemoteSessions?: boolean` - Enable Mission Control remote session support. Ignored when connecting via `RuntimeConnection.forUri`. @@ -358,6 +385,12 @@ Initial acquisition runs during session creation or resume. Cancellation, provid ##### `resumeSession(sessionId: string, config?: ResumeSessionConfig): Promise` Resume an existing session. Returns the session with `workspacePath` populated if infinite sessions were enabled. +`allowTranscriptRecovery` controls whether the runtime may repair a damaged transcript during +resume. It defaults to `true` in all modes; set it to `false` to reject recovery. +When repair occurs, inspect `session.transcriptRecovery` for the planned backup path, +invalid line numbers, and whether `session.start` was moved. The backup +is written when the repaired transcript is next appended, not during resume. Disabling recovery +still permits adding a missing newline after an intact final record; it rejects torn tails. ##### `ping(message?: string): Promise<{ message: string; timestamp: string }>` @@ -624,6 +657,24 @@ if (result.status === "pending") { See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for the full lifecycle rules. +##### `setTools(tools: Tool[]): Promise` + +Replace the tools this client supplies to the session, together with the handlers that serve them (experimental). `tools` takes the same definitions as `createSession` and becomes this client's complete tool set; pass `[]` to remove all of this client's tools. Built-in, MCP, and plugin tools, and tools other connected clients supply, are unaffected. + +Once the runtime accepts the replacement, every tool call this session dispatches uses the new handlers; calls already running finish on their original handlers. If the runtime rejects it, the promise rejects and the previous tools and handlers stay in place. Concurrent calls are applied one at a time, in call order. + +```typescript +await session.setTools([ + defineTool("search_issues", { + description: "Search the issues shown on the current page", + parameters: z.object({ query: z.string() }), + handler: async ({ query }) => searchIssues(query), + }), +]); +``` + +The agent sees the new tools from its next model request, which can fall within a turn in progress, so a model request already in flight can still call a tool you removed. See [Changing tools during a session](../docs/features/changing-tools.md) for the details. + ##### `abort(): Promise` Abort the currently processing message in this session. @@ -819,6 +870,22 @@ defineTool("edit_file", { }); ``` +An `apply_patch` override can declare a string schema. The model sees a required +`input` property, but the runtime restores the declared scalar shape before +dispatching to any SDK. Both a Zod-inferred Node handler and +`invocation.arguments` receive the patch text as a string: + +```ts +defineTool("apply_patch", { + parameters: z.string(), + overridesBuiltInTool: true, + handler: (patch) => patch.trim(), +}); +``` + +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 definition to allow it to execute without triggering a permission prompt: @@ -968,6 +1035,8 @@ Available section IDs: `preamble`, `identity`, `tone`, `tool_efficiency`, `envir `identity` and `tool_instructions` are section _groups_ that target a collection of related sub-sections as a unit. Use `preamble` to target just the identity preamble without affecting its sibling sub-sections. +`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. `runtime_instructions` is a separate section: removing it does not remove `last_instructions`. + Each section override supports five actions: - **`replace`** — Replace the section content entirely @@ -976,6 +1045,8 @@ Each section override supports five actions: - **`prepend`** — Add content before the existing section - **`preserve`** — No-op that opts an individually-addressable section out of a group-level `remove` +An `action` can also be a callback that receives the current section content and returns the replacement content, synchronously or asynchronously. + Unknown section IDs are handled gracefully: content from `replace`/`append`/`prepend` overrides is appended to additional instructions, and `remove` overrides are silently ignored. #### Replace Mode @@ -1423,6 +1494,20 @@ const session = await client.createSession({ }; } }, + + // Called before a sub-agent's first turn (the input identifies the parent session) + onSubagentStart: (input, invocation) => { + console.log(`Starting ${input.agentDisplayName ?? input.agentName}`); + return { additionalContext: "Check the requested file before reporting back." }; + }, + + // Called when a sub-agent completes a turn + onSubagentStop: (input, invocation) => { + console.log(`${input.agentName} replied: ${input.response}`); + // Return { decision: "block", reason: "Continue checking the file." } + // to request another child turn instead. + return { modifiedResponse: `Reviewed: ${input.response}` }; + }, }, }); ``` @@ -1437,6 +1522,8 @@ const session = await client.createSession({ - `onSessionEnd` - Cleanup or logging when session ends. - `onErrorOccurred` - Handle errors with retry/skip/abort strategies. - `onAgentStop` - Observe natural top-level agent completion. Return `{ decision: "block", reason }` to request another turn; use `stopHookActive` to avoid repeated blocks. +- `onSubagentStart` - Observe a sub-agent before its first turn and prepend `additionalContext` to the child's prompt. +- `onSubagentStop` - Observe a sub-agent's final response. Return `{ decision: "block", reason }` to request another child turn, or `{ modifiedResponse }` to replace the response reported to the parent. ## Error Handling diff --git a/nodejs/docs/agent-author.md b/nodejs/docs/agent-author.md index 6b9366a7e6..73875cea14 100644 --- a/nodejs/docs/agent-author.md +++ b/nodejs/docs/agent-author.md @@ -122,10 +122,12 @@ hooks: { onSessionStart: async (input, invocation) => { ... }, onSessionEnd: async (input, invocation) => { ... }, onErrorOccurred: async (input, invocation) => { ... }, + onSubagentStart: async (input, invocation) => { ... }, + onSubagentStop: async (input, invocation) => { ... }, } ``` -All hook inputs include `timestamp` (`Date`) and `workingDirectory`. +All hook inputs include `sessionId`, `timestamp` (`Date`) and `workingDirectory`. All handlers receive `invocation: { sessionId: string }` as the second argument. All handlers may return `void`/`undefined` (no-op) or an output object. @@ -214,7 +216,32 @@ fire it. | `retryCount` | `number` | Max retries (when errorHandling is "retry") | | `userNotification` | `string` | Message shown to the user | ---- +### onSubagentStart + +Fires before a sub-agent's first turn. The input's `sessionId` and the +invocation's `sessionId` identify the parent session, not the child. + +**Input:** `{ sessionId: string, transcriptPath: string, agentName: string, agentDisplayName?: string, agentDescription?: string, timestamp, workingDirectory }` + +**Output (optional):** +| Field | Type | Effect | +|-------|------|--------| +| `additionalContext` | `string` | Prepended to the child's initial prompt | + +### onSubagentStop + +Fires after a sub-agent completes a turn. The input includes the child's last +assistant `response` and the parent's session metadata. `agentId` is available +when the task registry supplies one. This is distinct from `onAgentStop`, which +only runs for the top-level agent. + +**Input:** `{ sessionId: string, transcriptPath: string, agentName: string, agentDisplayName?: string, agentDescription?: string, agentId?: string, agentType: string, stopReason: "end_turn", response: string, timestamp, workingDirectory }` + +**Output (choose one, or return nothing):** +| Field | Type | Effect | +|-------|------|--------| +| `decision` and `reason` | `"block"` and `string` | Continue the child for another turn using `reason` | +| `modifiedResponse` | `string` | Replace the child's response reported to the parent | ## Session Object diff --git a/nodejs/esbuild-copilotsdk-nodejs.ts b/nodejs/esbuild-copilotsdk-nodejs.ts index f65a47236f..d3c5030a4e 100644 --- a/nodejs/esbuild-copilotsdk-nodejs.ts +++ b/nodejs/esbuild-copilotsdk-nodejs.ts @@ -34,4 +34,4 @@ import { writeFileSync } from "fs"; writeFileSync("dist/cjs/package.json", JSON.stringify({ type: "commonjs" }) + "\n"); // Generate .d.ts files -execSync("tsc", { stdio: "inherit" }); +execSync("tsc --incremental --tsBuildInfoFile dist/tsconfig.tsbuildinfo", { stdio: "inherit" }); diff --git a/nodejs/package.json b/nodejs/package.json index 7748258c8c..ab2ea01ff2 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/github/copilot-sdk.git" }, "version": "0.0.0-dev", - "copilotCliVersion": "1.0.90-5", + "copilotCliVersion": "1.0.92-5", "description": "TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC", "main": "./dist/cjs/index.js", "types": "./dist/index.d.ts", @@ -36,7 +36,6 @@ "auth:refresh": "node ../scripts/npm-auth-refresh.mjs --run", "clean": "rimraf --glob dist *.tgz", "build": "tsx esbuild-copilotsdk-nodejs.ts", - "acquire:runtime-packages": "tsx scripts/runtime-package-acquisition.ts", "pack:release": "npm run verify:dependency-policy && tsx scripts/package-sdk.ts", "verify:dependency-policy": "tsx scripts/verify-dependency-policy.ts", "verify:release-packages": "tsx scripts/verify-release-packages.ts", @@ -98,6 +97,7 @@ }, "files": [ "dist/**/*", + "!dist/tsconfig.tsbuildinfo", "docs/**/*", "README.md" ] diff --git a/nodejs/scripts/package-release.ts b/nodejs/scripts/package-release.ts new file mode 100644 index 0000000000..90ad30615c --- /dev/null +++ b/nodejs/scripts/package-release.ts @@ -0,0 +1,378 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; +import { basename, dirname, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { createPackageSetManifest } from "./release-manifest.js"; +import { RUNTIME_PLATFORMS } from "../src/runtimeArtifacts.js"; +import { + checksums, + identityFromEnvironment, + rustReleaseLocation, + sealPackages, + seedRustRuntime, + releaseLanguages, + type ReleaseIdentity, +} from "./unified-release.js"; + +export const JAVA_CLASSIFIERS = { + "linux-arm64": "linux.arm64", + "linuxmusl-x64": "linuxmusl.x64", + "win32-x64": "win32", + "win32-arm64": "win32.arm64", + "darwin-arm64": "darwin", + "darwin-x64": "darwin.x64", +}; + +export function copyPackages(from: string, to: string, suffixes: string[]): string[] { + const matches = readdirSync(from).filter((file) => + suffixes.some((suffix) => file.endsWith(suffix)) + ); + assert(matches.length > 0, `No packages in ${from}`); + return matches.map((file) => { + const destination = join(to, file); + cpSync(join(from, file), destination); + return destination; + }); +} + +export function stampRustPackageManifest(manifest: string, version: string): string { + const stamped = manifest.replace(/^version = ".*"$/m, `version = "${version}"`); + // The selected product checkout is nested beneath another Cargo workspace. + // Keep this independent SDK out of both runtime workspaces during packaging. + return /^\[workspace(?:\.|\])/m.test(stamped) + ? stamped + : `${stamped.trimEnd()}\n\n[workspace]\n`; +} + +export function pythonPackageVersion(identity: ReleaseIdentity): string { + if (identity.channel !== "unstable") return identity.sdkVersion; + const match = /^(\d+\.\d+\.\d+)-unstable\.([1-9]\d*)\.g[0-9a-f]{7}$/.exec(identity.sdkVersion); + assert(match, "Expected an unstable SDK version with a run ID and source SHA"); + // PyPI rejects SemVer's unstable suffix and local (+sha) versions. + return `${match[1]}.dev${match[2]}`; +} + +/** Write pins with reviewed tooling without changing selected-source scripts or implementation. */ +export function writeRustReleaseSnapshots( + rust: string, + bundle: string, + identity: ReleaseIdentity, + run: (args: string[]) => void = (args) => { + execFileSync("bash", args, { cwd: rust, stdio: "inherit" }); + } +): void { + if (identity.channel === "unstable") { + const implementation = join(rust, "build", "in_process.rs"); + const source = existsSync(implementation) ? readFileSync(implementation, "utf8") : ""; + assert( + source.includes('"release-url"') && + source.includes("https://github.com/github/copilot-sdk/releases/download/runtime-"), + "Selected Rust source does not support SDK-hosted unstable runtime acquisition" + ); + } + const reviewedScripts = resolve(dirname(fileURLToPath(import.meta.url)), "../../rust/scripts"); + for (const [script, output] of [ + ["snapshot-bundled-cli-version.sh", "cli-version.txt"], + ["snapshot-bundled-in-process-version.sh", "cli-version-in-process.txt"], + ]) { + run([ + join(reviewedScripts, script), + "--version", + identity.runtimeVersion, + "--release-url", + rustReleaseLocation(identity).url, + "--checksums", + join(bundle, "github-release", "dist-bin", "SHA256SUMS.txt"), + "--output", + join(rust, output), + ]); + } +} + +export async function packageRelease( + language: string, + product: string, + bundle: string, + runtime: string, + output: string, + execute: (command: string, args: string[], cwd: string, env: NodeJS.ProcessEnv) => void = ( + command, + args, + cwd, + env + ) => { + execFileSync(command, args, { cwd, env, stdio: "inherit" }); + } +): Promise { + const identity = identityFromEnvironment(); + releaseLanguages(identity, language); + const sdk = join(product, "src", "sdk"); + const work = join(product, "sdk-release-work"); + mkdirSync(work, { recursive: true }); + const environment: NodeJS.ProcessEnv = { + ...process.env, + COPILOT_RUNTIME_SOURCE: "published", + COPILOT_SDK_RUNTIME_PACKAGE_DIR: runtime, + TMPDIR: work, + TMP: work, + TEMP: work, + VERSION: identity.sdkVersion, + }; + const run = (command: string, args: string[], cwd: string, env = environment) => + execute(command, args, cwd, env); + const node = (args: string[], cwd = sdk) => run(process.execPath, args, cwd); + const retained = (language: string) => { + const directory = join(output, language); + mkdirSync(directory, { recursive: true }); + return directory; + }; + const source = execFileSync("git", ["rev-parse", "HEAD"], { + cwd: product, + encoding: "utf8", + }).trim(); + assert.equal( + source, + identity.sourceSha, + "SDK product checkout is not the selected runtime source" + ); + for (const schema of ["api.schema.json", "session-events.schema.json"]) { + const expected = JSON.parse(readFileSync(join(product, "generated", schema), "utf8")); + for (const platform of RUNTIME_PLATFORMS) { + assert.deepEqual( + JSON.parse(readFileSync(join(runtime, platform, "schemas", schema), "utf8")), + expected, + `${platform} runtime schema ${schema} does not match selected SDK sources` + ); + } + } + const nodeRoot = join(sdk, "nodejs"); + node(["scripts/set-cli-version.js", identity.runtimeVersion, "--local-package"], nodeRoot); + node(["scripts/set-version.js"], nodeRoot); + if (language === "nodejs") { + const nodeManifestPath = join(nodeRoot, "package.json"); + const nodeManifest = JSON.parse(readFileSync(nodeManifestPath, "utf8")); + // npm trusted publishing validates the executing repository; private-source provenance is unavailable. + nodeManifest.repository = { + type: "git", + url: "https://github.com/github/copilot-agent-runtime.git", + directory: "src/sdk/nodejs", + }; + nodeManifest.copilotRuntime = { + sourceSha: identity.sourceSha, + version: identity.runtimeVersion, + visibility: identity.visibility, + }; + writeFileSync(nodeManifestPath, `${JSON.stringify(nodeManifest, null, 4)}\n`); + run("npm", ["run", "build"], nodeRoot); + run("npm", ["run", "pack:release"], nodeRoot); + run("npm", ["run", "verify:release-packages"], nodeRoot); + const nodeOutput = retained("nodejs"); + copyPackages(nodeRoot, nodeOutput, [".tgz"]); + writeFileSync( + join(nodeOutput, "package-set-manifest.json"), + `${JSON.stringify( + await createPackageSetManifest(nodeOutput, identity.sdkVersion), + null, + 2 + )}\n` + ); + } + + if (language === "python") { + const python = join(sdk, "python"); + const manifest = join(python, "pyproject.toml"); + writeFileSync( + manifest, + readFileSync(manifest, "utf8").replace( + /^version = ".*"$/m, + `version = "${pythonPackageVersion(identity)}"` + ) + ); + node(["scripts/inject-cli-version.mjs"], python); + const pythonDist = join(work, "python-dist"); + run("uv", ["build", "--wheel", "--out-dir", pythonDist], python); + const wheels = copyPackages(pythonDist, retained("python"), [".whl"]); + assert.equal(wheels.length, 1, "Expected one Python release wheel"); + } + if (language === "dotnet") { + run( + "dotnet", + [ + "pack", + "src/GitHub.Copilot.SDK.csproj", + "-c", + "Release", + `-p:Version=${identity.sdkVersion}`, + `-p:CopilotCliVersion=${identity.runtimeVersion}`, + "-p:CopilotSkipCliDownload=true", + `-p:RepositoryCommit=${identity.sourceSha}`, + "-p:RepositoryUrl=https://github.com/github/copilot-agent-runtime", + `-p:PackageOutputPath=${retained("dotnet")}`, + ], + join(sdk, "dotnet") + ); + } + if (language === "rust") { + const rust = join(sdk, "rust"); + const manifest = join(rust, "Cargo.toml"); + writeFileSync( + manifest, + stampRustPackageManifest(readFileSync(manifest, "utf8"), identity.sdkVersion) + ); + writeRustReleaseSnapshots(rust, bundle, identity, (args) => run("bash", args, rust)); + const cache = join(work, "rust-cache"); + seedRustRuntime(bundle, cache, identity); + const rustEnvironment: NodeJS.ProcessEnv = { + ...environment, + BUNDLED_CLI_CACHE_DIR: cache, + COPILOT_CLI_EXTRACT_DIR: join(work, "rust-runtime"), + CARGO_TARGET_DIR: join(rust, "target"), + }; + assert.equal( + rustEnvironment.COPILOT_SKIP_CLI_DOWNLOAD, + undefined, + "Rust acquisition verification cannot skip downloads" + ); + assert.equal( + rustEnvironment.DOCS_RS, + undefined, + "Rust acquisition verification cannot use docs.rs mode" + ); + // Refresh the stamped workspace version without upgrading locked dependencies. + run("cargo", ["update", "--workspace"], rust, rustEnvironment); + run("cargo", ["package", "--locked", "--allow-dirty"], rust, rustEnvironment); + const rustOutput = retained("rust"); + copyPackages(join(rust, "target", "package"), rustOutput, [".crate"]); + run( + "tar", + ["-czf", join(rustOutput, "source.tar.gz"), "--exclude=rust/target", "-C", sdk, "rust"], + sdk + ); + } + if (language === "java") { + const java = join(sdk, "java"); + const javaOutput = retained("java"); + const release = join(bundle, "github-release"); + const hashes = checksums(readFileSync(join(release, "dist-bin", "SHA256SUMS.txt"), "utf8")); + const nativeArgs: string[] = []; + for (const [classifier, property] of Object.entries(JAVA_CLASSIFIERS)) { + const asset = `github-copilot-${identity.runtimeVersion}-${classifier}.tgz`; + const staging = join(work, "java-native"); + const jar = join( + work, + `copilot-sdk-java-runtime-${identity.sdkVersion}-${classifier}.jar` + ); + run( + process.execPath, + ["copilot-native/scripts/fetch-native.mjs", sdk, staging, classifier], + java, + { + ...environment, + COPILOT_CLI_RELEASE_TARBALL: join(release, "dist-pkg-tarballs", asset), + COPILOT_CLI_RELEASE_SHA256: hashes.get(asset)!, + } + ); + run( + "jar", + ["--create", "--file", jar, "-C", join(staging, classifier), "native"], + java + ); + node( + [ + "copilot-native/scripts/validate-native-artifact.mjs", + "classifier", + classifier, + jar, + basename(jar), + sdk, + ], + java + ); + nativeArgs.push(`-Dcopilot.native.external.${property}.classifier.path=${jar}`); + } + const linuxAsset = `github-copilot-${identity.runtimeVersion}-linux-x64.tgz`; + const mavenRepository = join(work, "maven-repository"); + // Retain packaging outputs and source; native Maven deployment rebuilds and signs from that source. + run( + "bash", + [ + "mvnw", + "-B", + "install", + "-Prelease", + "-DskipTests", + "-DskipITs", + `-Drevision=${identity.sdkVersion}`, + "-Dcopilot.native.libc=glibc", + `-Dmaven.repo.local=${mavenRepository}`, + ...nativeArgs, + ], + java, + { + ...environment, + COPILOT_CLI_RELEASE_TARBALL: join(release, "dist-pkg-tarballs", linuxAsset), + COPILOT_CLI_RELEASE_SHA256: hashes.get(linuxAsset)!, + } + ); + for (const artifact of ["copilot-sdk-java", "copilot-sdk-java-runtime"]) { + const coordinate = join("com", "github", artifact, identity.sdkVersion); + const destination = join(javaOutput, artifact); + mkdirSync(destination, { recursive: true }); + for (const file of readdirSync(join(mavenRepository, coordinate))) { + if (!/\.(jar|pom|asc)$/.test(file)) continue; + const path = join(destination, file); + cpSync(join(mavenRepository, coordinate, file), path); + } + for (const file of readdirSync(destination).filter((name) => + /\.(jar|pom)$/.test(name) + )) { + assert( + readFileSync(join(destination, `${file}.asc`)).length > 0, + `Missing signature: ${file}` + ); + } + } + cpSync(join(release, "dist-pkg-tarballs", linuxAsset), join(javaOutput, linuxAsset)); + cpSync(join(nodeRoot, "package.json"), join(javaOutput, "nodejs-package.json")); + run( + "tar", + [ + "-czf", + join(javaOutput, "source.tar.gz"), + "--exclude=*/target", + "--exclude=*/node_modules", + "-C", + sdk, + "java", + ], + sdk + ); + } + sealPackages(output, identity, language); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + const args = process.argv.slice(2); + if (args[0] === "--help") { + console.log(`Usage: package-release.ts +Builds and verifies one selected SDK without registry/source publication. +Run unified-release.ts stage-runtime first. Required environment: +SOURCE_SHA, RUNTIME_VERSION, SDK_VERSION, CHANNEL, VISIBILITY. +Normal Java packages require an imported GPG key and JAVA_GPG_PASSPHRASE.`); + } else { + assert.equal( + args.length, + 5, + "Expected language, product, runtime bundle, staged runtime and output paths; use --help" + ); + await packageRelease( + args[0], + ...(args.slice(1).map((value) => resolve(value)) as [string, string, string, string]) + ); + } +} diff --git a/nodejs/scripts/publish-release.ts b/nodejs/scripts/publish-release.ts new file mode 100644 index 0000000000..292926d409 --- /dev/null +++ b/nodejs/scripts/publish-release.ts @@ -0,0 +1,374 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { cpSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { x as extractTar } from "tar"; +import * as semver from "semver"; +import { + identityFromEnvironment, + sha256, + verifyPackages, + type ReleaseIdentity, +} from "./unified-release.js"; +import { JAVA_CLASSIFIERS } from "./package-release.js"; + +type NativeCommand = ( + command: string, + args: string[], + cwd: string, + env?: NodeJS.ProcessEnv +) => string; + +const runNativeCommand: NativeCommand = (command, args, cwd, env = process.env) => + execFileSync(command, args, { + cwd, + env, + encoding: "utf8", + stdio: command === "git" ? "pipe" : "inherit", + }) ?? ""; + +const runGh = (args: string[]): string => + execFileSync("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim(); + +/** Record the independent SDK baseline in the runtime repository's source history. */ +export function recordRuntimeSdkVersion(identity: ReleaseIdentity, gh = runGh): void { + assert.equal(identity.visibility, "public"); + assert.notEqual( + identity.channel, + "unstable", + "Only normal public SDK releases create baseline tags" + ); + const repository = "github/copilot-agent-runtime"; + const tag = `sdk-${identity.sdkVersion}`; + let reference; + try { + reference = JSON.parse(gh(["api", `repos/${repository}/git/ref/tags/${tag}`])); + } catch (error) { + if (!/\(HTTP 404\)/.test(String((error as { stderr?: string }).stderr))) throw error; + } + if (reference) { + assert.equal(reference.object.type, "tag", `Existing ${tag} must be annotated`); + const existing = JSON.parse( + gh(["api", `repos/${repository}/git/tags/${reference.object.sha}`]) + ); + assert.equal(existing.tag, tag, "Existing SDK tag has another name"); + assert.equal( + existing.object.type, + "commit", + "SDK tag must point directly to the release source" + ); + assert.equal( + existing.object.sha, + identity.sourceSha, + `Existing ${tag} points to another source` + ); + assert( + Number.isFinite(Date.parse(existing.tagger?.date)), + "Existing SDK tag has no valid publication time" + ); + return; + } + const annotated = JSON.parse( + gh([ + "api", + "--method", + "POST", + `repos/${repository}/git/tags`, + "-f", + `tag=${tag}`, + "-f", + `message=SDK ${identity.sdkVersion}`, + "-f", + `object=${identity.sourceSha}`, + "-f", + "type=commit", + "-f", + "tagger[name]=github-actions[bot]", + "-f", + "tagger[email]=41898282+github-actions[bot]@users.noreply.github.com", + "-f", + `tagger[date]=${new Date().toISOString()}`, + ]) + ); + assert.match(annotated.sha, /^[0-9a-f]{40}$/, "GitHub did not return an annotated tag object"); + gh([ + "api", + "--method", + "POST", + `repos/${repository}/git/refs`, + "-f", + `ref=refs/tags/${tag}`, + "-f", + `sha=${annotated.sha}`, + ]); +} + +async function restoreNativeSource( + directory: string, + identity: ReleaseIdentity, + product: string, + language: "rust" | "java", + run: NativeCommand +): Promise { + assert(product, "Native publishing requires the selected product checkout"); + assert.equal( + run("git", ["rev-parse", "HEAD"], product).trim(), + identity.sourceSha, + "Native publishing checkout does not match the selected product SHA" + ); + const sdk = join(product, "src", "sdk"); + const destination = join(sdk, language); + rmSync(destination, { recursive: true, force: true }); + mkdirSync(sdk, { recursive: true }); + await extractTar({ + file: join(directory, "source.tar.gz"), + cwd: sdk, + strict: true, + preservePaths: false, + filter: (path) => { + assert( + path.startsWith(`${language}/`), + "Retained native source must stay in its product directory" + ); + return true; + }, + }); + return destination; +} + +/** Preserve native Cargo repackaging from retained source, lockfile, and runtime pins. */ +export async function publishRust( + directory: string, + identity: ReleaseIdentity, + product: string, + run: NativeCommand = runNativeCommand +): Promise { + const archives = readdirSync(directory).filter((file) => file.endsWith(".crate")); + assert.deepEqual( + archives, + [`github-copilot-sdk-${identity.sdkVersion}.crate`], + "Wrong retained Rust version" + ); + const rust = await restoreNativeSource(directory, identity, product, "rust", run); + assert(readFileSync(join(rust, "Cargo.lock")).length > 0, "Missing retained Cargo.lock"); + assert.equal( + /^version = "([^"]+)"$/m.exec(readFileSync(join(rust, "Cargo.toml"), "utf8"))?.[1], + identity.sdkVersion, + "Retained Rust SDK version differs" + ); + for (const file of ["cli-version.txt", "cli-version-in-process.txt"]) { + assert.equal( + /^version=(.+)$/m.exec(readFileSync(join(rust, file), "utf8"))?.[1], + identity.runtimeVersion, + `Retained runtime pin differs in ${file}` + ); + } + run( + "cargo", + [ + "publish", + "--locked", + "--allow-dirty", + "--no-verify", + "--target-dir", + join(rust, "target"), + ], + rust + ); +} + +/** Native Maven deploy rebuilds and signs retained source with the retained classifier inputs. */ +export async function publishJava( + directory: string, + identity: ReleaseIdentity, + product: string, + run: NativeCommand = runNativeCommand +): Promise { + const java = await restoreNativeSource(directory, identity, product, "java", run); + const nodeManifest = join(directory, "nodejs-package.json"); + assert.equal( + JSON.parse(readFileSync(nodeManifest, "utf8")).copilotCliVersion, + identity.runtimeVersion, + "Retained Java runtime pin differs" + ); + cpSync(nodeManifest, join(product, "src", "sdk", "nodejs", "package.json")); + const runtimeArchive = join( + directory, + `github-copilot-${identity.runtimeVersion}-linux-x64.tgz` + ); + const nativeArgs = Object.entries(JAVA_CLASSIFIERS).map( + ([classifier, property]) => + `-Dcopilot.native.external.${property}.classifier.path=${join( + directory, + "copilot-sdk-java-runtime", + `copilot-sdk-java-runtime-${identity.sdkVersion}-${classifier}.jar` + )}` + ); + run( + "bash", + [ + "mvnw", + "-B", + "deploy", + "-Prelease", + "-DskipTests", + "-DskipITs", + `-Drevision=${identity.sdkVersion}`, + "-Dcopilot.native.libc=glibc", + ...nativeArgs, + ], + java, + { + ...process.env, + COPILOT_CLI_RELEASE_TARBALL: runtimeArchive, + COPILOT_CLI_RELEASE_SHA256: sha256(runtimeArchive), + } + ); +} + +export function previousSdkReleaseTag( + releases: { + tag_name: string; + draft: boolean; + prerelease: boolean; + published_at: string | null; + }[], + identity: ReleaseIdentity +): string | undefined { + return releases + .filter( + (release) => + !release.draft && + release.published_at && + release.tag_name !== `v${identity.sdkVersion}` && + release.tag_name.startsWith("v") && + semver.valid(release.tag_name.slice(1)) && + (identity.channel !== "latest" || !release.prerelease) + ) + .sort((left, right) => Date.parse(right.published_at!) - Date.parse(left.published_at!))[0] + ?.tag_name; +} + +export async function publishSourceRelease( + identity: ReleaseIdentity, + snapshot: string, + gh: (args: string[]) => string = runGh +): Promise { + assert.equal(identity.visibility, "public"); + assert.match( + snapshot, + /^[0-9a-f]{40}$/, + "Expected exact exported public SDK commit, not the runtime SHA" + ); + assert.notEqual( + snapshot, + identity.sourceSha, + "SDK source tags cannot reference the private runtime SHA" + ); + const repository = "github/copilot-sdk"; + for (const tag of [ + `v${identity.sdkVersion}`, + ...["go", "rust", "java"].map((language) => `${language}/v${identity.sdkVersion}`), + ]) { + let reference; + try { + reference = JSON.parse(gh(["api", `repos/${repository}/git/ref/tags/${tag}`])); + } catch (error) { + if (!/\(HTTP 404\)/.test(String((error as { stderr?: string }).stderr))) throw error; + } + if (reference) { + let object: { type: string; sha: string } = reference.object; + for (let depth = 0; object.type === "tag" && depth < 10; depth++) { + object = JSON.parse( + gh(["api", `repos/${repository}/git/tags/${object.sha}`]) + ).object; + } + assert.equal(object.type, "commit", `Cannot resolve source tag ${tag}`); + assert.equal(object.sha, snapshot, `Existing tag ${tag} points to another source`); + } else { + gh([ + "api", + "--method", + "POST", + `repos/${repository}/git/refs`, + "-f", + `ref=refs/tags/${tag}`, + "-f", + `sha=${snapshot}`, + ]); + } + } + if (identity.channel === "unstable") return; + let release; + try { + release = JSON.parse( + gh(["api", `repos/${repository}/releases/tags/v${identity.sdkVersion}`]) + ); + } catch (error) { + if (!/\(HTTP 404\)/.test(String((error as { stderr?: string }).stderr))) throw error; + } + if (release) { + assert.equal( + release.prerelease, + identity.channel === "prerelease", + "Existing SDK release has another channel" + ); + assert.equal(release.draft, false, "Existing SDK release is a draft"); + return; + } + const releases = JSON.parse( + gh(["api", "--paginate", "--slurp", `repos/${repository}/releases?per_page=100`]) + ).flat(); + const previous = previousSdkReleaseTag(releases, identity); + gh([ + "release", + "create", + `v${identity.sdkVersion}`, + "--repo", + repository, + "--verify-tag", + "--title", + `v${identity.sdkVersion}`, + "--generate-notes", + ...(previous ? ["--notes-start-tag", previous] : []), + ...(identity.channel === "prerelease" ? ["--prerelease", "--latest=false"] : ["--latest"]), + ]); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + const [command, directory, flag] = process.argv.slice(2); + if (command === "--help") { + console.log(`Usage: publish-release.ts --run + publish-release.ts record-version --run +Publishes only with --run, after validating the retained release manifest. +Required environment: SOURCE_SHA, RUNTIME_VERSION, SDK_VERSION, CHANNEL, VISIBILITY. +Rust: CARGO_REGISTRY_TOKEN and PRODUCT_CHECKOUT (checked out at SOURCE_SHA). +Java: PRODUCT_CHECKOUT, Maven Central settings, and imported GPG key with JAVA_GPG_PASSPHRASE. +Source release: GH_TOKEN and SDK_SNAPSHOT_SHA (the exported public commit).`); + } else if (command === "record-version") { + assert.equal(directory, "--run", "Publication requires --run"); + assert.equal(flag, undefined, "Unexpected record-version argument"); + recordRuntimeSdkVersion(identityFromEnvironment()); + } else { + assert.equal(flag, "--run", "Publication requires --run; use --help for usage"); + const identity = identityFromEnvironment(); + assert.equal( + identity.visibility, + "public", + "This helper never publishes internal releases" + ); + verifyPackages(resolve(directory), identity); + if (command === "rust") + await publishRust(resolve(directory, "rust"), identity, process.env.PRODUCT_CHECKOUT!); + else if (command === "java") { + await publishJava(resolve(directory, "java"), identity, process.env.PRODUCT_CHECKOUT!); + } else if (command === "source") + await publishSourceRelease(identity, process.env.SDK_SNAPSHOT_SHA!); + else throw new Error(`Unknown publisher: ${command}`); + } +} diff --git a/nodejs/scripts/release-manifest.ts b/nodejs/scripts/release-manifest.ts index 580d245600..d1448e1690 100644 --- a/nodejs/scripts/release-manifest.ts +++ b/nodejs/scripts/release-manifest.ts @@ -1,7 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + import assert from "node:assert/strict"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { basename, join, resolve } from "node:path"; +import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { globSync } from "glob"; import * as semver from "semver"; @@ -11,7 +14,6 @@ import { SDK_PACKAGE_NAMES, verifyPackageSetManifestFiles, } from "./package-set-manifest.js"; -import { validateRuntimeReleaseIdentity } from "./runtime-release-identity.js"; import { getRuntimePackageName, RUNTIME_PLATFORMS } from "../src/runtimeArtifacts.js"; export interface ReleaseManifestPackage { @@ -29,43 +31,6 @@ export interface PackageSetManifest { }; } -export interface ReleaseManifest extends PackageSetManifest { - channel: "canary" | "unstable"; - runtime: { - repository: "github/copilot-agent-runtime"; - runId: string; - sha: string; - source: "github-packages"; - version: string; - }; - sdk: { - ref: string; - repository: "github/copilot-sdk"; - sha: string; - version: string; - }; - workflow: { - createdAt: string; - runId: string; - runNumber: string; - testPolicy: "advisory" | "required" | "skipped"; - }; -} - -export interface ReleaseManifestMetadata { - channel: ReleaseManifest["channel"]; - createdAt: string; - runtimeSha: string; - runtimeRunId: string; - runtimeVersion: string; - sdkRef: string; - sdkSha: string; - sdkVersion: string; - testPolicy: ReleaseManifest["workflow"]["testPolicy"]; - workflowRunId: string; - workflowRunNumber: string; -} - const expectedPackageNames = new Set(SDK_PACKAGE_NAMES); assert.deepEqual( [...expectedPackageNames].sort(), @@ -74,7 +39,7 @@ assert.deepEqual( ); async function readPackedManifest(archive: string): Promise<{ name: string; version: string }> { - const root = mkdtempSync(join(tmpdir(), "copilot-sdk-release-manifest-")); + const root = mkdtempSync(join(dirname(archive), ".copilot-sdk-release-manifest-")); try { await extractTar({ cwd: root, @@ -91,48 +56,6 @@ async function readPackedManifest(archive: string): Promise<{ name: string; vers } } -function validateFullSha(value: string, label: string): void { - assert.match(value, /^[0-9a-f]{40}$/i, `${label} must be a full 40-character SHA`); -} - -export async function createReleaseManifest( - packageDirectory: string, - metadata: ReleaseManifestMetadata -): Promise { - validateFullSha(metadata.sdkSha, "SDK SHA"); - validateRuntimeReleaseIdentity({ - channel: metadata.channel, - runId: metadata.runtimeRunId, - sha: metadata.runtimeSha, - version: metadata.runtimeVersion, - }); - assert(Number.isFinite(Date.parse(metadata.createdAt)), "Workflow creation time is invalid"); - const packageSet = await createPackageSetManifest(packageDirectory, metadata.sdkVersion); - return { - ...packageSet, - channel: metadata.channel, - sdk: { - ...packageSet.sdk, - sha: metadata.sdkSha, - ref: metadata.sdkRef, - repository: "github/copilot-sdk", - }, - runtime: { - version: metadata.runtimeVersion, - sha: metadata.runtimeSha, - source: "github-packages", - repository: "github/copilot-agent-runtime", - runId: metadata.runtimeRunId, - }, - workflow: { - runId: metadata.workflowRunId, - runNumber: metadata.workflowRunNumber, - createdAt: metadata.createdAt, - testPolicy: metadata.testPolicy, - }, - }; -} - export async function createPackageSetManifest( packageDirectory: string, sdkVersion: string @@ -178,36 +101,6 @@ export function verifyPackageSetManifest( assert(semver.valid(manifest.sdk.version), "Invalid SDK version"); } -export function verifyReleaseManifest(manifest: ReleaseManifest, packageDirectory: string): void { - verifyPackageSetManifest(manifest, packageDirectory); - assert( - manifest.channel === "canary" || manifest.channel === "unstable", - "Invalid release channel" - ); - validateFullSha(manifest.sdk.sha, "SDK SHA"); - validateRuntimeReleaseIdentity({ - channel: manifest.channel, - runId: manifest.runtime.runId, - sha: manifest.runtime.sha, - version: manifest.runtime.version, - }); - assert.match(manifest.workflow.runId, /^[0-9]+$/, "Invalid SDK workflow run ID"); - assert.match(manifest.workflow.runNumber, /^[0-9]+$/, "Invalid SDK workflow run number"); - assert( - manifest.workflow.testPolicy === "required" || - manifest.workflow.testPolicy === "advisory" || - manifest.workflow.testPolicy === "skipped", - "Invalid runtime E2E test policy" - ); - assert( - Number.isFinite(Date.parse(manifest.workflow.createdAt)), - "Invalid workflow creation time" - ); - assert.equal(manifest.sdk.repository, "github/copilot-sdk"); - assert.equal(manifest.runtime.repository, "github/copilot-agent-runtime"); - assert.equal(manifest.runtime.source, "github-packages", "Invalid runtime package source"); -} - function requiredEnvironment(name: string): string { const value = process.env[name]?.trim(); if (!value) { @@ -228,38 +121,13 @@ async function main(): Promise { verifyPackageSetManifest(manifest, packageDirectory); return; } - if (command === "create") { - const manifest = await createReleaseManifest(packageDirectory, { - channel: requiredEnvironment("RELEASE_CHANNEL") as ReleaseManifest["channel"], - createdAt: requiredEnvironment("WORKFLOW_CREATED_AT"), - runtimeSha: requiredEnvironment("RUNTIME_SHA"), - runtimeRunId: requiredEnvironment("RUNTIME_RUN_ID"), - runtimeVersion: requiredEnvironment("RUNTIME_VERSION"), - sdkRef: requiredEnvironment("SDK_REF"), - sdkSha: requiredEnvironment("SDK_SHA"), - sdkVersion: requiredEnvironment("SDK_VERSION"), - testPolicy: requiredEnvironment( - "TEST_POLICY" - ) as ReleaseManifest["workflow"]["testPolicy"], - workflowRunId: requiredEnvironment("WORKFLOW_RUN_ID"), - workflowRunNumber: requiredEnvironment("WORKFLOW_RUN_NUMBER"), - }); - writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); - verifyReleaseManifest(manifest, packageDirectory); - return; - } - if (command === "verify") { - const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as ReleaseManifest; - verifyReleaseManifest(manifest, packageDirectory); - return; - } if (command === "verify-package-set") { const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as PackageSetManifest; verifyPackageSetManifest(manifest, packageDirectory); return; } throw new Error( - "Usage: release-manifest.ts create|verify|create-package-set|verify-package-set [manifest-path] [package-directory]" + "Usage: release-manifest.ts create-package-set|verify-package-set [manifest-path] [package-directory]" ); } diff --git a/nodejs/scripts/releaseArtifacts.ts b/nodejs/scripts/releaseArtifacts.ts index f68c8bddb1..2746f3bfd4 100644 --- a/nodejs/scripts/releaseArtifacts.ts +++ b/nodejs/scripts/releaseArtifacts.ts @@ -18,6 +18,7 @@ import { validateFile, } from "../src/runtimeArtifacts.js"; import { COPILOT_CLI_USE_NPM_PACKAGE, COPILOT_CLI_VERSION } from "../src/cliVersion.js"; +import { runtimeReleaseUrl } from "../../scripts/runtime-release.mjs"; export interface EnsureCopilotPackageOptions { cacheRoot?: string; @@ -32,19 +33,18 @@ export async function downloadVerifiedReleaseAsset( assetName: string, options: Omit = {} ): Promise { - const baseUrl = ( - (options.environment ?? process.env).COPILOT_CLI_DOWNLOAD_BASE_URL ?? - "https://github.com/github/copilot-cli/releases/download" - ).replace(/\/+$/, ""); + const releaseUrl = runtimeReleaseUrl( + version, + (options.environment ?? process.env).COPILOT_CLI_DOWNLOAD_BASE_URL ?? null + ); const fetcher = options.fetch ?? globalThis.fetch; if (!fetcher) { throw new Error("This Node.js runtime does not provide fetch()."); } const fetchTimeoutMs = options.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS; const expectedChecksum = await getReleaseChecksum( - version, assetName, - baseUrl, + releaseUrl, fetcher, fetchTimeoutMs ); @@ -53,7 +53,7 @@ export async function downloadVerifiedReleaseAsset( } const archive = await fetchWithRetry( fetcher, - `${baseUrl}/v${version}/${assetName}`, + `${releaseUrl}/${assetName}`, async (response) => Buffer.from(await response.arrayBuffer()), fetchTimeoutMs ); @@ -124,16 +124,15 @@ async function fetchWithRetry( } async function getReleaseChecksum( - version: string, assetName: string, - baseUrl: string, + releaseUrl: string, fetcher: typeof globalThis.fetch, fetchTimeoutMs: number ): Promise { - const key = `${baseUrl}\0${version}`; + const key = releaseUrl; let checksums = checksumDownloads.get(key); if (!checksums) { - checksums = downloadReleaseChecksums(version, baseUrl, fetcher, fetchTimeoutMs); + checksums = downloadReleaseChecksums(releaseUrl, fetcher, fetchTimeoutMs); checksumDownloads.set(key, checksums); try { return (await checksums).get(assetName); @@ -146,14 +145,13 @@ async function getReleaseChecksum( } async function downloadReleaseChecksums( - version: string, - baseUrl: string, + releaseUrl: string, fetcher: typeof globalThis.fetch, fetchTimeoutMs: number ): Promise> { const contents = await fetchWithRetry( fetcher, - `${baseUrl}/v${version}/SHA256SUMS.txt`, + `${releaseUrl}/SHA256SUMS.txt`, (response) => response.text(), fetchTimeoutMs ); @@ -202,12 +200,10 @@ export async function ensureCopilotPackage( return cachedPackageRoot; } - const baseUrl = ( - environment.COPILOT_CLI_DOWNLOAD_BASE_URL ?? - "https://github.com/github/copilot-cli/releases/download" - ).replace(/\/+$/, ""); + const baseUrl = environment.COPILOT_CLI_DOWNLOAD_BASE_URL; + const releaseUrl = runtimeReleaseUrl(version, baseUrl ?? null); const fetchTimeoutMs = options.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS; - const key = `${cacheRoot}\0${version}\0${platform}\0${baseUrl}`; + const key = `${cacheRoot}\0${version}\0${platform}\0${releaseUrl}`; if (!options.fetch) { const existing = packageDownloads.get(key); if (existing) { @@ -242,7 +238,7 @@ async function downloadCopilotPackage( version: string, platform: string, cacheRoot: string, - baseUrl: string, + baseUrl: string | undefined, fetcher: typeof globalThis.fetch, fetchTimeoutMs: number ): Promise { diff --git a/nodejs/scripts/runtime-package-acquisition.ts b/nodejs/scripts/runtime-package-acquisition.ts deleted file mode 100644 index b30c1877a6..0000000000 --- a/nodejs/scripts/runtime-package-acquisition.ts +++ /dev/null @@ -1,251 +0,0 @@ -import assert from "node:assert/strict"; -import { createHash } from "node:crypto"; -import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"; -import { basename, join, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; -import { parseArgs } from "node:util"; -import { x as extractTar } from "tar"; -import { runCommand } from "./npm-release.js"; -import { RUNTIME_PLATFORMS, validateFile } from "../src/runtimeArtifacts.js"; - -interface CommandResult { - status: number; - stderr: string; - stdout: string; -} - -interface RuntimePackageManifest { - copilotRuntime?: { - sourceRepository?: string; - sourceSha?: string; - }; - cpu?: string[]; - libc?: string[]; - name?: string; - os?: string[]; - repository?: string | { url?: string }; - version?: string; -} - -export interface AcquireRuntimePackagesOptions { - outputDirectory: string; - runtimeSha: string; - runtimeVersion: string; -} - -export type CommandRunner = (command: string, args: string[]) => Promise; - -const GITHUB_PACKAGES_REGISTRY = "https://npm.pkg.github.com"; - -export function getSourceRuntimePackageName(platform: string): string { - return `@github/copilot-${platform}`; -} - -function parseJsonOutput(result: CommandResult, description: string): T { - if (result.status !== 0) { - throw new Error( - `${description} failed with exit code ${result.status}: ${result.stderr || result.stdout}` - ); - } - try { - return JSON.parse(result.stdout) as T; - } catch { - throw new Error(`${description} returned invalid JSON: ${result.stdout}`); - } -} - -function validatePlatformMetadata(manifest: RuntimePackageManifest, platform: string): void { - const [osName, cpu] = platform.replace("linuxmusl", "linux").split("-"); - assert.deepEqual(manifest.os, [osName], `Invalid os metadata for ${platform}`); - assert.deepEqual(manifest.cpu, [cpu], `Invalid cpu metadata for ${platform}`); - if (platform.startsWith("linux")) { - assert.deepEqual( - manifest.libc, - [platform.startsWith("linuxmusl") ? "musl" : "glibc"], - `Invalid libc metadata for ${platform}` - ); - } else { - assert.equal(manifest.libc, undefined, `Unexpected libc metadata for ${platform}`); - } -} - -function repositoryUrl(repository: RuntimePackageManifest["repository"]): string { - return typeof repository === "string" ? repository : (repository?.url ?? ""); -} - -export function validateRuntimePackageRoot( - packageRoot: string, - platform: string, - runtimeVersion: string, - runtimeSha: string -): void { - const manifestPath = join(packageRoot, "package.json"); - validateFile(manifestPath, `${platform} runtime package manifest`); - const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as RuntimePackageManifest; - assert.equal(manifest.name, getSourceRuntimePackageName(platform)); - assert.equal(manifest.version, runtimeVersion); - assert.equal(manifest.copilotRuntime?.sourceRepository, "github/copilot-agent-runtime"); - assert.equal(manifest.copilotRuntime?.sourceSha, runtimeSha.toLowerCase()); - assert( - repositoryUrl(manifest.repository).includes("github/copilot-agent-runtime"), - `${manifest.name} does not link to github/copilot-agent-runtime` - ); - validatePlatformMetadata(manifest, platform); - - const windows = platform.startsWith("win32"); - for (const requiredPath of [ - "LICENSE.md", - windows ? "copilot.exe" : "copilot", - join("prebuilds", platform, windows ? "copilot-runtime.exe" : "copilot-runtime"), - join("prebuilds", platform, "runtime.node"), - join("copilot-sdk", "extension.js"), - join("preloads", "extension_bootstrap.mjs"), - ]) { - validateFile(join(packageRoot, requiredPath), `${manifest.name} ${requiredPath}`); - } -} - -function sha512Integrity(path: string): string { - return `sha512-${createHash("sha512").update(readFileSync(path)).digest("base64")}`; -} - -export async function acquireRuntimePackages( - options: AcquireRuntimePackagesOptions, - runner: CommandRunner = runCommand -): Promise { - assert.match(options.runtimeSha, /^[0-9a-f]{40}$/, "Runtime SHA must be lowercase full SHA"); - assert( - options.outputDirectory.trim().length > 0, - "Runtime package output directory is required" - ); - const outputDirectory = resolve(options.outputDirectory); - const tarballDirectory = join(outputDirectory, "tarballs"); - mkdirSync(tarballDirectory, { recursive: true }); - const acquired: { - filename: string; - integrity: string; - name: string; - platform: string; - version: string; - }[] = []; - - for (const platform of RUNTIME_PLATFORMS) { - const packageName = getSourceRuntimePackageName(platform); - const spec = `${packageName}@${options.runtimeVersion}`; - const viewResult = await runner("npm", [ - "view", - spec, - "dist.integrity", - "--json", - "--registry", - GITHUB_PACKAGES_REGISTRY, - ]); - const registryIntegrity = parseJsonOutput( - viewResult, - `Reading registry integrity for ${spec}` - ); - assert.match( - registryIntegrity, - /^sha512-[A-Za-z0-9+/]+={0,2}$/, - `Invalid registry integrity for ${spec}` - ); - const packResult = await runner("npm", [ - "pack", - spec, - "--json", - "--pack-destination", - tarballDirectory, - "--registry", - GITHUB_PACKAGES_REGISTRY, - ]); - const packed = parseJsonOutput<{ filename: string; integrity?: string }[]>( - packResult, - `Downloading ${spec}` - ); - assert.equal(packed.length, 1, `npm pack returned an unexpected result for ${spec}`); - const tarball = join(tarballDirectory, basename(packed[0].filename)); - validateFile(tarball, `${spec} tarball`); - assert.equal(sha512Integrity(tarball), registryIntegrity, `Integrity mismatch for ${spec}`); - if (packed[0].integrity) { - assert.equal( - packed[0].integrity, - registryIntegrity, - `npm pack integrity mismatch for ${spec}` - ); - } - - const extractionRoot = join(outputDirectory, `.extract-${platform}`); - const packageRoot = join(extractionRoot, "package"); - rmSync(extractionRoot, { recursive: true, force: true }); - mkdirSync(extractionRoot, { recursive: true }); - try { - await extractTar({ cwd: extractionRoot, file: tarball, strict: true }); - validateRuntimePackageRoot( - packageRoot, - platform, - options.runtimeVersion, - options.runtimeSha - ); - const destination = join(outputDirectory, platform); - rmSync(destination, { recursive: true, force: true }); - renameSync(packageRoot, destination); - } finally { - rmSync(extractionRoot, { recursive: true, force: true }); - } - acquired.push({ - filename: basename(tarball), - integrity: registryIntegrity, - name: packageName, - platform, - version: options.runtimeVersion, - }); - } - - assert.equal(acquired.length, 8); - writeFileSync( - join(outputDirectory, "runtime-packages.json"), - `${JSON.stringify( - { - runtimeVersion: options.runtimeVersion, - runtimeSha: options.runtimeSha, - registry: GITHUB_PACKAGES_REGISTRY, - packages: acquired, - }, - null, - 2 - )}\n` - ); -} - -export function parseArguments(args: string[]): AcquireRuntimePackagesOptions { - const { tokens, values } = parseArgs({ - args, - allowPositionals: false, - options: { - output: { type: "string" }, - sha: { type: "string" }, - version: { type: "string" }, - }, - strict: true, - tokens: true, - }); - for (const name of ["version", "sha", "output"] as const) { - const occurrences = tokens.filter( - (token) => token.kind === "option" && token.name === name - ); - assert.equal(occurrences.length, 1, `Option --${name} must be provided exactly once`); - assert(values[name]?.trim(), `Option --${name} requires a non-empty value`); - } - return { - runtimeVersion: values.version!, - runtimeSha: values.sha!, - outputDirectory: values.output!, - }; -} - -if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) { - acquireRuntimePackages(parseArguments(process.argv.slice(2))).catch((error) => { - console.error(`::error::${error instanceof Error ? error.message : String(error)}`); - process.exitCode = 1; - }); -} diff --git a/nodejs/scripts/runtime-release-identity.ts b/nodejs/scripts/runtime-release-identity.ts deleted file mode 100644 index 6e932b740a..0000000000 --- a/nodejs/scripts/runtime-release-identity.ts +++ /dev/null @@ -1,213 +0,0 @@ -import assert from "node:assert/strict"; -import { appendFileSync } from "node:fs"; -import { resolve } from "node:path"; -import { fileURLToPath } from "node:url"; -import * as semver from "semver"; - -export type ReleaseDistTag = "canary" | "latest" | "prerelease" | "unstable"; -export type ReleaseMode = "dry-run" | "publish"; -export type ReleaseTestPolicy = "advisory" | "required" | "skipped"; -export type RuntimeReleaseChannel = "canary" | "unstable"; - -export interface ReleaseDispatchInputs { - distTag: ReleaseDistTag; - mode: ReleaseMode; - runtimeJson: string; - testPolicy: ReleaseTestPolicy; - version: string; -} - -export interface ReleaseDispatchPlan { - kind: "direct" | "runtime"; - runtimeRunId: string; - runtimeSha: string; - runtimeVersion: string; - testPolicy: ReleaseTestPolicy; -} - -interface RuntimeDescriptor { - run_id: string; - sha: string; - version: string; -} - -export interface RuntimeReleaseIdentity { - channel: RuntimeReleaseChannel; - runId: string; - sha: string; - version: string; -} - -export function validateRuntimeReleaseIdentity(identity: RuntimeReleaseIdentity): void { - const { channel, runId, sha, version } = identity; - assert(channel === "canary" || channel === "unstable", "Invalid channel"); - assert.match(runId, /^[1-9][0-9]*$/, "Runtime run_id must be a positive canonical integer"); - assert.match(sha, /^[0-9a-f]{40}$/, "Runtime sha must be a lowercase full SHA"); - const parsed = semver.parse(version); - assert(parsed, "Runtime version must be exact SemVer"); - assert.equal(parsed.build.length, 0, "Runtime version must not contain build metadata"); - assert.equal(version, parsed.version, "Runtime version must be exact SemVer"); - - const channelIndex = typeof parsed.prerelease[0] === "number" ? 1 : 0; - assert.equal( - parsed.prerelease[channelIndex], - channel, - `Runtime version '${version}' does not use the '${channel}' channel identifier` - ); - assert.equal( - parsed.prerelease[channelIndex + 1], - `r${runId}`, - `Runtime version '${version}' does not match run_id '${runId}'` - ); - assert.equal( - parsed.prerelease[channelIndex + 2], - `g${sha.slice(0, 7)}`, - `Runtime version '${version}' does not match runtime sha '${sha}'` - ); - - if (channel === "canary") { - assert.equal( - parsed.prerelease.length, - channelIndex + 4, - "Canary runtime version must end with exactly one signing identifier" - ); - assert( - parsed.prerelease[channelIndex + 3] === "signed" || - parsed.prerelease[channelIndex + 3] === "unsigned", - "Canary runtime version must end with signed or unsigned" - ); - } else { - assert.equal( - parsed.prerelease.length, - channelIndex + 3, - "Unstable runtime version must not contain a signing or additional suffix" - ); - } -} - -function parseRuntimeDescriptor(value: string): RuntimeDescriptor { - let parsed: unknown; - try { - parsed = JSON.parse(value); - } catch { - throw new Error("Runtime input must be valid JSON."); - } - assert( - typeof parsed === "object" && parsed !== null && !Array.isArray(parsed), - "Runtime input must be a JSON object" - ); - assert.deepEqual( - Object.keys(parsed).sort(), - ["run_id", "sha", "version"], - "Runtime input must contain exactly version, sha, and run_id" - ); - const runtime = parsed as Partial; - for (const name of ["version", "sha", "run_id"] as const) { - assert.equal(typeof runtime[name], "string", `Runtime ${name} must be a string`); - } - return runtime as RuntimeDescriptor; -} - -export function validateReleaseDispatch(inputs: ReleaseDispatchInputs): ReleaseDispatchPlan { - assert( - inputs.distTag === "latest" || - inputs.distTag === "prerelease" || - inputs.distTag === "unstable" || - inputs.distTag === "canary", - "Invalid release dist-tag" - ); - assert(inputs.mode === "publish" || inputs.mode === "dry-run", "Invalid release mode"); - assert( - inputs.testPolicy === "required" || - inputs.testPolicy === "advisory" || - inputs.testPolicy === "skipped", - "Invalid runtime E2E test policy" - ); - - if (inputs.runtimeJson === "") { - assert.equal( - inputs.testPolicy, - "required", - "Direct releases require the default runtime E2E test policy" - ); - assert(inputs.distTag !== "canary", "Canary releases require runtime JSON"); - assert( - inputs.mode !== "dry-run" || inputs.distTag === "unstable", - "Dry-run mode is supported only for canary and unstable releases" - ); - return { - kind: "direct", - runtimeRunId: "", - runtimeSha: "", - runtimeVersion: "", - testPolicy: inputs.testPolicy, - }; - } - - assert.equal( - inputs.runtimeJson, - inputs.runtimeJson.trim(), - "Runtime input must not contain surrounding whitespace" - ); - assert( - inputs.distTag === "canary" || inputs.distTag === "unstable", - "Runtime JSON is supported only for canary and unstable releases" - ); - assert.equal( - inputs.version, - "", - "The direct version input cannot be combined with runtime JSON" - ); - const runtime = parseRuntimeDescriptor(inputs.runtimeJson); - validateRuntimeReleaseIdentity({ - channel: inputs.distTag, - runId: runtime.run_id, - sha: runtime.sha, - version: runtime.version, - }); - return { - kind: "runtime", - runtimeRunId: runtime.run_id, - runtimeSha: runtime.sha, - runtimeVersion: runtime.version, - testPolicy: inputs.testPolicy, - }; -} - -function requiredEnvironment(name: string): string { - const value = process.env[name]; - if (value === undefined || value === "") { - throw new Error(`${name} is required.`); - } - return value; -} - -function main(): void { - const plan = validateReleaseDispatch({ - distTag: requiredEnvironment("DIST_TAG") as ReleaseDistTag, - mode: requiredEnvironment("MODE") as ReleaseMode, - runtimeJson: process.env.RUNTIME_JSON ?? "", - testPolicy: requiredEnvironment("TEST_POLICY") as ReleaseTestPolicy, - version: process.env.VERSION_OVERRIDE ?? "", - }); - appendFileSync( - requiredEnvironment("GITHUB_OUTPUT"), - [ - `kind=${plan.kind}`, - `runtime_run_id=${plan.runtimeRunId}`, - `runtime_sha=${plan.runtimeSha}`, - `runtime_version=${plan.runtimeVersion}`, - `test_policy=${plan.testPolicy}`, - "", - ].join("\n") - ); -} - -if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) { - try { - main(); - } catch (error) { - console.error(`::error::${error instanceof Error ? error.message : String(error)}`); - process.exitCode = 1; - } -} diff --git a/nodejs/scripts/unified-release.ts b/nodejs/scripts/unified-release.ts new file mode 100644 index 0000000000..56b1b2ddae --- /dev/null +++ b/nodejs/scripts/unified-release.ts @@ -0,0 +1,598 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + cpSync, + mkdirSync, + readFileSync, + readdirSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import { basename, join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import * as semver from "semver"; +import { x as extractTar } from "tar"; +import { calculateVersion } from "./calculate-version.js"; +import { assertVersionAbsent, getRegistryVersion } from "./npm-release.js"; +import { calculateUnstableVersion } from "./unstable-version.js"; +import { RUNTIME_PLATFORMS, validateFile } from "../src/runtimeArtifacts.js"; +import { checkGenerated } from "../../scripts/ci/check-generated.mjs"; + +export interface ReleaseIdentity { + sourceSha: string; + runtimeVersion: string; + sdkVersion: string; + channel: "latest" | "prerelease" | "unstable"; + visibility: "public" | "internal"; +} + +const sdkNames = { + nodejs: "Node.js", + python: "Python", + go: "Go", + dotnet: ".NET", + rust: "Rust", + java: "Java", +}; + +export type SdkLanguage = keyof typeof sdkNames; +export type PackagedSdkLanguage = Exclude; + +// https://github.com/github/copilot-sdk/releases/tag/v1.0.15-preview.1 +// Its exported SDK tree matches CLI 1.0.89-0, apart from the runtime version pins. +const SDK_BOOTSTRAP_BASELINE = { + version: "1.0.15-preview.1", + sourceSha: "898cd62f80fc25d28b190d0682111a5418eacbd7", + publishedAt: "2026-09-22T22:26:49Z", +}; + +export function selectedSdks(channel: string, visibility: string): SdkLanguage[] { + assert(["latest", "prerelease", "unstable"].includes(channel), "Invalid SDK channel"); + assert(["public", "internal"].includes(visibility), "Invalid SDK visibility"); + assert(visibility === "public" || channel === "unstable", "Internal releases must be unstable"); + if (visibility === "public") return ["nodejs", "python", "go", "dotnet", "rust", "java"]; + return ["nodejs"]; +} + +export function packagedSdks(channel: string, visibility: string): PackagedSdkLanguage[] { + return selectedSdks(channel, visibility).filter((language) => language !== "go"); +} + +export function packageMatrix( + channel: string, + visibility: string +): { + include: { language: PackagedSdkLanguage; name: string }[]; +} { + return { + include: packagedSdks(channel, visibility).map((language) => ({ + language, + name: sdkNames[language], + })), + }; +} + +export function publicationSdks( + channel: string, + visibility: string, + destination: string +): PackagedSdkLanguage[] { + assert( + destination === "public" || destination === "internal", + "Invalid publication destination" + ); + assert( + destination !== "public" || visibility === "public", + "Internal releases cannot publish publicly" + ); + // Python publishes in the caller so PyPI authentication and attestations share one workflow identity. + return packagedSdks(channel, visibility).filter( + (language) => + language !== "python" && + (destination === "public" || + language === "nodejs" || + (channel !== "unstable" && language === "dotnet")) + ); +} + +export function releaseLanguages( + identity: ReleaseIdentity, + language?: string +): PackagedSdkLanguage[] { + const selected = packagedSdks(identity.channel, identity.visibility); + if (language === undefined) return selected; + const matched = selected.find((candidate) => candidate === language); + assert( + matched, + `SDK ${language} is not selected for ${identity.channel}/${identity.visibility}` + ); + return [matched]; +} + +export function planSdkVersion({ + channel, + visibility, + sourceSha, + runId, + override = "", + versions, +}: Omit & { + runId: string; + override?: string; + versions: { latest: string; prerelease?: string }; +}): string { + selectedSdks(channel, visibility); + assert.notEqual(channel, "unstable", "Unstable SDK versions require source-history planning"); + assert.match(sourceSha, /^[0-9a-f]{40}$/, "Expected a full product source SHA"); + assert.match(runId, /^[1-9][0-9]*$/, "Expected a workflow run ID"); + const published = Object.values(versions).filter( + (version): version is string => version !== undefined + ); + for (const version of published) { + assert.equal(semver.valid(version), version, `Invalid registry version: ${version}`); + } + const version = + override || + calculateVersion(channel, { + latest: versions.latest, + prerelease: versions.prerelease, + }); + assert.equal(semver.valid(version), version, "SDK version must be canonical SemVer"); + assert.equal(semver.parse(version)!.build.length, 0, "SDK version cannot have build metadata"); + assert.equal( + semver.prerelease(version) === null, + channel === "latest", + "SDK version does not match channel" + ); + return version; +} + +export function planGitUnstableSdkVersion( + repository: string, + sourceSha: string, + createdAt: string, + runId: string, + bootstrap = SDK_BOOTSTRAP_BASELINE +): string { + assert.match(sourceSha, /^[0-9a-f]{40}$/, "Expected a full product source SHA"); + const git = (args: string[]) => + execFileSync("git", args, { + cwd: repository, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }).trim(); + const commits = git(["rev-list", "--first-parent", sourceSha]).split(/\r?\n/); + const positions = new Map(commits.map((sha, index) => [sha, index])); + const tags = git([ + "for-each-ref", + "--format=%(refname:short)%09%(objecttype)%09%(objectname)%09%(*objectname)%09%(*objecttype)%09%(taggerdate:iso-strict)", + "refs/tags/sdk-*", + ]) + .split(/\r?\n/) + .filter(Boolean) + .flatMap((line) => { + const [tag, type, object, target, targetType, date] = line.split("\t"); + const version = tag.slice("sdk-".length); + if (semver.valid(version) !== version) return []; + if (!positions.has(type === "tag" ? target : object)) return []; + assert( + type === "tag" && targetType === "commit", + `SDK release tag ${tag} must be an annotated tag pointing directly to a commit` + ); + assert( + Number.isFinite(Date.parse(date)), + `SDK release tag ${tag} has no valid tagger timestamp` + ); + return [{ version, target, date }]; + }); + if ( + !tags.some((tag) => Date.parse(tag.date) <= Date.parse(createdAt)) && + positions.has(bootstrap.sourceSha) && + Date.parse(bootstrap.publishedAt) <= Date.parse(createdAt) + ) { + tags.push({ + version: bootstrap.version, + target: bootstrap.sourceSha, + date: bootstrap.publishedAt, + }); + } + tags.sort( + (left, right) => + positions.get(left.target)! - positions.get(right.target)! || + semver.rcompare(left.version, right.version) + ); + return calculateUnstableVersion({ + createdAt, + sdkSha: sourceSha, + runId, + firstParentTags: tags.map((tag) => `v${tag.version}`), + releases: tags.map((tag) => ({ tag_name: `v${tag.version}`, published_at: tag.date })), + }); +} + +export function checkReleaseSources( + product: string, + channel: string, + visibility: string, + run: (args: string[]) => void = (args) => { + execFileSync(process.execPath, args, { cwd: product, stdio: "inherit", windowsHide: true }); + } +): void { + const languages = selectedSdks(channel, visibility); + checkGenerated({ + root: product, + baseline: undefined, + generateSchemas: () => + run([ + "src/sdk/scripts/run-tasks.mjs", + "generate:schemas", + "--runtime-source", + "checkout", + ]), + generateSdk: () => { + if (visibility === "public") { + run(["src/sdk/scripts/run-tasks.mjs", "generate", "--runtime-source", "checkout"]); + return; + } + for (const language of languages) { + run([ + "src/sdk/scripts/run-tasks.mjs", + "generate", + language, + "--runtime-source", + "checkout", + ]); + } + }, + // All six protocol constants are inexpensive Node-generated files, requiring no language toolchains. + generateProtocol: () => run(["src/sdk/nodejs/scripts/update-protocol-version.ts"]), + }); +} + +export function validateIdentity(identity: ReleaseIdentity): void { + selectedSdks(identity.channel, identity.visibility); + assert.match(identity.sourceSha, /^[0-9a-f]{40}$/, "Invalid product SHA"); + for (const version of [identity.runtimeVersion, identity.sdkVersion]) { + assert.equal(semver.valid(version), version, "Invalid release version"); + } + if (identity.channel === "unstable") { + assert( + semver.prerelease(identity.sdkVersion)?.[0] === "unstable", + "SDK version does not use the unstable channel" + ); + } else { + assert.equal( + semver.prerelease(identity.sdkVersion) === null, + identity.channel === "latest", + "SDK version does not match channel" + ); + } +} + +export function checksums(text: string): Map { + const result = new Map(); + for (const line of text.trim().split(/\r?\n/)) { + const match = /^([0-9a-f]{64})\s+\*?([^/\\\s]+)$/i.exec(line); + assert(match, `Invalid checksum line: ${line}`); + assert(!result.has(match[2]), `Duplicate checksum for ${match[2]}`); + result.set(match[2], match[1].toLowerCase()); + } + return result; +} + +export function sha256(file: string): string { + return createHash("sha256").update(readFileSync(file)).digest("hex"); +} + +export function verifyAsset(file: string, hashes: Map): void { + const expected = hashes.get(basename(file)); + assert(expected, `Missing checksum: ${basename(file)}`); + assert.equal(sha256(file), expected, `Checksum mismatch: ${basename(file)}`); +} + +export function rustReleaseLocation(identity: ReleaseIdentity): { + url: string; + cachePrefix: string; +} { + validateIdentity(identity); + assert.equal(identity.visibility, "public", "Private Rust acquisition is not supported"); + return identity.channel === "unstable" + ? { + url: `https://github.com/github/copilot-sdk/releases/download/runtime-${identity.runtimeVersion}`, + cachePrefix: `copilot-sdk-runtime-${identity.runtimeVersion}`, + } + : { + url: `https://github.com/github/copilot-cli/releases/download/v${identity.runtimeVersion}`, + cachePrefix: `v${identity.runtimeVersion}`, + }; +} + +export function seedRustRuntime(bundle: string, cache: string, identity: ReleaseIdentity): void { + const { cachePrefix } = rustReleaseLocation(identity); + const release = join(bundle, "github-release"); + const hashes = checksums(readFileSync(join(release, "dist-bin", "SHA256SUMS.txt"), "utf8")); + mkdirSync(cache, { recursive: true }); + for (const [directory, asset] of [ + ["dist-bin", "copilot-linux-x64.tar.gz"], + ["dist-pkg-tarballs", `github-copilot-${identity.runtimeVersion}-linux-x64.tgz`], + ]) { + const file = join(release, directory, asset); + verifyAsset(file, hashes); + cpSync(file, join(cache, `${cachePrefix}-${asset}`)); + } +} + +/** Only accepts the signed, same-run release bundle; never falls back to a published release. */ +export async function stageRuntime( + bundle: string, + output: string, + identity: ReleaseIdentity +): Promise { + validateIdentity(identity); + const release = join(bundle, "github-release"); + const recorded = JSON.parse(readFileSync(join(release, "release-identity.json"), "utf8")); + assert.equal(recorded.signed, true, "SDK packaging requires signed runtime artifacts"); + assert.equal(recorded.sourceSha, identity.sourceSha, "Runtime bundle has a different source"); + assert.equal(recorded.channel, identity.channel, "Runtime bundle has a different channel"); + assert.equal( + recorded.visibility, + identity.visibility, + "Runtime bundle has a different visibility" + ); + assert.equal( + recorded.runtimeVersion, + identity.runtimeVersion, + "Runtime bundle has a different version" + ); + const hashes = checksums(readFileSync(join(release, "dist-bin", "SHA256SUMS.txt"), "utf8")); + for (const platform of RUNTIME_PLATFORMS) { + const archive = join( + release, + "dist-pkg-tarballs", + `github-copilot-${identity.runtimeVersion}-${platform}.tgz` + ); + verifyAsset(archive, hashes); + const destination = join(output, platform); + rmSync(destination, { force: true, recursive: true }); + mkdirSync(destination, { recursive: true }); + await extractTar({ + file: archive, + cwd: destination, + strip: 1, + strict: true, + preservePaths: false, + }); + const manifest = JSON.parse(readFileSync(join(destination, "package.json"), "utf8")); + assert.equal( + manifest.version, + identity.runtimeVersion, + `Wrong runtime version for ${platform}` + ); + if (manifest.copilotRuntime) { + assert.equal( + manifest.copilotRuntime.sourceSha, + identity.sourceSha, + `Wrong runtime SHA for ${platform}` + ); + } + for (const file of [ + "LICENSE.md", + join( + "prebuilds", + platform, + platform.startsWith("win32") ? "copilot-runtime.exe" : "copilot-runtime" + ), + join("prebuilds", platform, "runtime.node"), + join("copilot-sdk", "extension.js"), + join("preloads", "extension_bootstrap.mjs"), + ]) + validateFile(join(destination, file), `${platform} ${file}`); + const executable = `copilot-${platform}.${platform.startsWith("win32") ? "zip" : "tar.gz"}`; + verifyAsset(join(release, "dist-bin", executable), hashes); + } +} + +export function filesUnder(root: string): string[] { + return readdirSync(root, { withFileTypes: true }).flatMap((entry) => { + const file = join(root, entry.name); + assert(!entry.isSymbolicLink(), `Release artifacts cannot contain symlinks: ${file}`); + return entry.isDirectory() ? filesUnder(file) : [file]; + }); +} + +export function sealPackages( + directory: string, + identity: ReleaseIdentity, + language?: string +): void { + validateIdentity(identity); + const languages = releaseLanguages(identity, language); + const packages = filesUnder(directory) + .filter((file) => file !== join(directory, "release-manifest.json")) + .map((file) => ({ + filename: file.slice(directory.length + 1).replaceAll("\\", "/"), + sha256: sha256(file), + size: statSync(file).size, + })); + for (const file of packages) { + assert( + languages.some((sdk) => file.filename.startsWith(`${sdk}/`)), + `Unexpected SDK artifact: ${file.filename}` + ); + } + for (const sdk of languages) { + assert( + packages.some((file) => file.filename.startsWith(`${sdk}/`)), + `Missing ${sdk} release artifacts` + ); + } + writeFileSync( + join(directory, "release-manifest.json"), + `${JSON.stringify({ schemaVersion: 1, ...identity, language, packages }, null, 2)}\n` + ); +} + +export function verifyPackages( + directory: string, + identity: ReleaseIdentity, + language?: string +): void { + validateIdentity(identity); + const languages = releaseLanguages(identity, language); + const manifest = JSON.parse(readFileSync(join(directory, "release-manifest.json"), "utf8")); + for (const [key, value] of Object.entries(identity)) + assert.equal(manifest[key], value, `Retained ${key} mismatch`); + assert.equal(manifest.schemaVersion, 1); + assert.equal(manifest.language, language, "Retained SDK selection mismatch"); + assert( + Array.isArray(manifest.packages) && manifest.packages.length > 0, + "Empty retained package set" + ); + for (const file of manifest.packages) { + assert( + typeof file.filename === "string" && + !file.filename.includes("\\") && + file.filename + .split("/") + .every((part: string) => part && part !== "." && part !== "..") && + !file.filename.includes(":"), + "Unsafe retained artifact filename" + ); + assert( + languages.some((sdk) => file.filename.startsWith(`${sdk}/`)), + `Unexpected SDK artifact: ${file.filename}` + ); + const path = join(directory, file.filename); + assert.equal(statSync(path).size, file.size, `Retained size mismatch: ${file.filename}`); + assert.equal(sha256(path), file.sha256, `Retained hash mismatch: ${file.filename}`); + } + for (const sdk of languages) { + assert( + manifest.packages.some((file: { filename: string }) => + file.filename.startsWith(`${sdk}/`) + ), + `Missing ${sdk} release artifacts` + ); + } + assert.deepEqual( + filesUnder(directory) + .filter((file) => file !== join(directory, "release-manifest.json")) + .map((file) => file.slice(directory.length + 1).replaceAll("\\", "/")) + .sort(), + manifest.packages.map((file: { filename: string }) => file.filename).sort(), + "Unexpected retained files" + ); +} + +/** Join independently validated language artifacts without changing publisher inputs. */ +export function assemblePackages(parts: string, output: string, identity: ReleaseIdentity): void { + const languages = packagedSdks(identity.channel, identity.visibility); + assert.deepEqual( + readdirSync(parts).sort(), + languages.map((language) => `sdk-package-${language}`).sort(), + "Incomplete or unexpected SDK packaging artifacts" + ); + // Validate every part before copying any package into the assembled release. + for (const language of languages) { + verifyPackages(join(parts, `sdk-package-${language}`), identity, language); + } + mkdirSync(output, { recursive: true }); + assert.equal(readdirSync(output).length, 0, "Assembled SDK output must be empty"); + for (const language of languages) { + cpSync(join(parts, `sdk-package-${language}`, language), join(output, language), { + recursive: true, + }); + } + sealPackages(output, identity); +} + +export function identityFromEnvironment(sdkVersion = process.env.SDK_VERSION!): ReleaseIdentity { + const identity = { + sourceSha: process.env.SOURCE_SHA!, + runtimeVersion: process.env.RUNTIME_VERSION!, + sdkVersion, + channel: process.env.CHANNEL as ReleaseIdentity["channel"], + visibility: process.env.VISIBILITY as ReleaseIdentity["visibility"], + }; + validateIdentity(identity); + return identity; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + const [command, ...args] = process.argv.slice(2); + if (command === "--help") { + console.log(`Usage: unified-release.ts check-sources | languages | package-matrix | plan [product] | stage-runtime | assemble | seal | verify +Plans or validates release artifacts without publishing. +Required environment: SOURCE_SHA, RUNTIME_VERSION, SDK_VERSION, CHANNEL, VISIBILITY. +Normal planning uses public npm channel metadata; SDK_VERSION is an optional override. +Unstable planning uses annotated sdk-* Git tags or the source-anchored bootstrap baseline; it requires a selected product checkout and WORKFLOW_CREATED_AT, never npm baselines. +Stage-runtime accepts only the signed same-run cli-release-bundle, including release-identity.json and checksums.`); + } else if (command === "check-sources") { + checkReleaseSources(resolve(args[0]), process.env.CHANNEL!, process.env.VISIBILITY!); + } else if (command === "languages") { + console.log( + JSON.stringify( + publicationSdks( + process.env.CHANNEL!, + process.env.VISIBILITY!, + process.env.DESTINATION! + ) + ) + ); + } else if (command === "package-matrix") { + console.log(JSON.stringify(packageMatrix(process.env.CHANNEL!, process.env.VISIBILITY!))); + } else if (command === "plan") { + let sdkVersion: string; + if (process.env.CHANNEL === "unstable") { + assert(!process.env.SDK_VERSION, "Unstable SDK versions cannot be overridden"); + assert(args[1], "Unstable planning requires the selected product checkout"); + sdkVersion = planGitUnstableSdkVersion( + resolve(args[1]), + process.env.SOURCE_SHA!, + process.env.WORKFLOW_CREATED_AT!, + process.env.GITHUB_RUN_ID! + ); + } else { + const [latest, prerelease] = await Promise.all( + ["latest", "prerelease"].map((tag) => + getRegistryVersion("@github/copilot-sdk", tag, "https://registry.npmjs.org") + ) + ); + assert(latest, "No latest SDK version exists"); + sdkVersion = planSdkVersion({ + channel: process.env.CHANNEL as ReleaseIdentity["channel"], + visibility: process.env.VISIBILITY as ReleaseIdentity["visibility"], + sourceSha: process.env.SOURCE_SHA!, + runId: process.env.GITHUB_RUN_ID!, + override: process.env.SDK_VERSION, + versions: { latest, prerelease }, + }); + } + if (process.env.VISIBILITY === "public") { + await assertVersionAbsent( + "@github/copilot-sdk", + sdkVersion, + "https://registry.npmjs.org" + ); + } + writeFileSync(args[0], `${JSON.stringify(identityFromEnvironment(sdkVersion), null, 2)}\n`); + } else if (command === "stage-runtime") { + await stageRuntime(resolve(args[0]), resolve(args[1]), identityFromEnvironment()); + } else if (command === "assemble") { + assemblePackages(resolve(args[0]), resolve(args[1]), identityFromEnvironment()); + } else if (command === "seal") { + sealPackages(resolve(args[0]), identityFromEnvironment()); + } else if (command === "verify") { + verifyPackages(resolve(args[0]), identityFromEnvironment()); + } else { + throw new Error( + "Usage: unified-release.ts check-sources | languages | package-matrix | plan [product] | stage-runtime | assemble | seal | verify " + ); + } +} diff --git a/nodejs/src/canvas.ts b/nodejs/src/canvas.ts index aeb1f00ec4..b7c2254c4f 100644 --- a/nodejs/src/canvas.ts +++ b/nodejs/src/canvas.ts @@ -49,7 +49,12 @@ export interface CanvasAction { description?: string; /** Optional JSON Schema for the action's `input` payload. */ inputSchema?: CanvasJsonSchema; - /** Required per-action dispatch handler. */ + /** + * Required per-action dispatch handler. The returned value becomes the + * `invoke_canvas_action` tool result. Return a `ToolResultObject` + * (with `binaryResultsForLlm`) to send text and images to the model, as a + * tool handler would; any other value is rendered to the model as JSON text. + */ handler: (ctx: CanvasProviderInvokeActionRequest) => Promise | unknown; } diff --git a/nodejs/src/cliVersion.ts b/nodejs/src/cliVersion.ts index 74f9dd2b26..6338d1e0d2 100644 --- a/nodejs/src/cliVersion.ts +++ b/nodejs/src/cliVersion.ts @@ -1,3 +1,3 @@ -export const COPILOT_CLI_VERSION = "1.0.90-5"; +export const COPILOT_CLI_VERSION = "1.0.92-5"; export const COPILOT_CLI_USE_NPM_PACKAGE = false; diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 32af18890a..24f7f6ff2e 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -98,6 +98,7 @@ import type { SystemMessageCustomizeConfig, TelemetryConfig, TraceContextProvider, + TranscriptRecovery, TypedSessionLifecycleHandler, } from "./types.js"; import { defaultJoinSessionPermissionHandler } from "./types.js"; @@ -113,6 +114,7 @@ interface ExtensionOrchestrationContributions { */ const MIN_PROTOCOL_VERSION = 3; const RUNTIME_SHUTDOWN_TIMEOUT_MS = 10_000; +const CLOUD_SESSION_CLEANUP_TIMEOUT_MS = 10_000; type DrainingMessageConnection = MessageConnection & { drain: () => Promise }; @@ -494,6 +496,7 @@ export class CopilotClient { private requestAdapter: ReturnType | null = null; private messageWriter: TeardownResilientStreamMessageWriter | null = null; private connectionClosed: boolean = false; + private connectionEpoch = 0; private socket: Socket | null = null; private runtimePort: number | null = null; private actualHost: string = "localhost"; @@ -897,6 +900,14 @@ export class CopilotClient { "SessionFsConfig declares capabilities.sqlite but the provider does not implement sqlite." ); } + if ( + this.sessionFsConfig.capabilities?.binary && + (!provider.readFileBytes || !provider.writeFileBytes) + ) { + throw new Error( + "SessionFsConfig declares capabilities.binary but the provider does not implement readFileBytes and writeFileBytes." + ); + } session.clientSessionApis.sessionFs = createSessionFsAdapter(provider); } @@ -1027,6 +1038,7 @@ export class CopilotClient { private async doStart(): Promise { this.forceStopping = false; + this.connectionEpoch++; this.connectionClosed = false; this.processTransportError = null; this.state = "connecting"; @@ -1077,6 +1089,9 @@ export class CopilotClient { await this.connection!.sendRequest("llmInference.setProvider", {}); } + if (this.connectionClosed) { + throw new Error("CLI server connection closed during startup"); + } this.state = "connected"; } catch (error) { const startupError = this.processTransportError ?? error; @@ -1233,10 +1248,24 @@ export class CopilotClient { this.socket = null; try { if (!socket.destroyed) { - await new Promise((resolve) => { - socket.once("close", () => resolve()); - socket.end(); - }); + let timeout: ReturnType | undefined; + try { + await new Promise((resolve) => { + socket.once("close", () => resolve()); + if (this.connectionClosed) { + // An exited child may leave a TCP peer that never finishes closing. + socket.destroy(); + } else { + timeout = setTimeout( + () => socket.destroy(), + RUNTIME_SHUTDOWN_TIMEOUT_MS + ); + socket.end(); + } + }); + } finally { + if (timeout !== undefined) clearTimeout(timeout); + } } } catch (error) { errors.push( @@ -1722,13 +1751,14 @@ export class CopilotClient { if (config.onEvent) { s.on(config.onEvent); } - this.sessions.set(sessionId, s); this.setupSessionFs(s, config); + this.sessions.set(sessionId, s); return s; }; let session: CopilotSession | undefined; let registeredId: string | undefined; + let uninitializedCloudSessionId: string | undefined; // Pre-register non-cloud sessions BEFORE issuing the RPC so any // session-scoped requests the CLI emits during `session.create` @@ -1868,7 +1898,9 @@ export class CopilotClient { if (session === undefined) { // Cloud / server-assigned path: register the session now that // the CLI has told us which id it chose. + uninitializedCloudSessionId = returnedSessionId; session = initializeSession(returnedSessionId); + uninitializedCloudSessionId = undefined; registeredId = returnedSessionId; } this.assignGitHubTokenProvider(gitHubTokenProviderRegistrationId, returnedSessionId); @@ -1891,6 +1923,20 @@ export class CopilotClient { if (gitHubTokenProviderRegistrationId !== undefined) { this.githubTokenProviders.delete(gitHubTokenProviderRegistrationId); } + if (uninitializedCloudSessionId !== undefined) { + try { + await withTimeout( + this.deleteSession(uninitializedCloudSessionId), + CLOUD_SESSION_CLEANUP_TIMEOUT_MS, + `session.delete timed out after ${CLOUD_SESSION_CLEANUP_TIMEOUT_MS}ms` + ); + } catch (cleanupError) { + throw new AggregateError( + [e, cleanupError], + "Failed to initialize and delete cloud session" + ); + } + } throw e; } @@ -2011,8 +2057,8 @@ export class CopilotClient { if (config.onEvent) { session.on(config.onEvent); } - this.sessions.set(sessionId, session); this.setupSessionFs(session, config); + this.sessions.set(sessionId, session); const toolFilterOptions = this.resolveToolFilterOptions(config); const gitHubTokenProviderRegistrationId = this.registerGitHubTokenProvider( @@ -2030,6 +2076,7 @@ export class CopilotClient { allowedModels: config.allowedModels, ...(await getTraceContext(this.onGetTraceContext)), sessionId, + allowTranscriptRecovery: config.allowTranscriptRecovery, clientName: config.clientName, model: config.model, reasoningEffort: config.reasoningEffort, @@ -2153,15 +2200,17 @@ export class CopilotClient { } } - const { workspacePath, capabilities, openCanvases } = response as { + const { workspacePath, capabilities, openCanvases, transcriptRecovery } = response as { sessionId: string; workspacePath?: string; capabilities?: SessionCapabilities; openCanvases?: OpenCanvasInstance[]; + transcriptRecovery?: TranscriptRecovery; }; session["_workspacePath"] = workspacePath; session.setCapabilities(capabilities); session.setOpenCanvases(openCanvases ?? []); + session.setTranscriptRecovery(transcriptRecovery); if (config.onMcpAuthRequest) { await this.connection!.sendRequest("session.eventLog.registerInterest", { sessionId, @@ -2196,23 +2245,24 @@ export class CopilotClient { * SDK connection to the same runtime, not another runtime process. * Only one AHP host may own the catalog in an effective Copilot home * at a time. Other SDK clients and sessions remain usable in that home. - * The runtime validates listener options and defaults to 127.0.0.1 on an - * available port. `onExit` runs at most once; owner disconnection cannot + * Select localServer, githubEnvironment, or both. An empty localServer + * selects 127.0.0.1 on an available port. `onExit` runs at most once; owner disconnection cannot * acknowledge listener cleanup over the disconnected transport. * * @experimental */ - async startAhpHost(options: AhpHostOptions = {}): Promise { + async startAhpHost(options: AhpHostOptions): Promise { + if (!options?.localServer && !options?.githubEnvironment) { + throw new Error("At least one of localServer or githubEnvironment is required"); + } if (this.state !== "connected") { await this.start(); } const rpc = this.rpc; const hostId = randomUUID(); const { - hostname, - port, - token, - requireConnectionToken, + localServer, + githubEnvironment, onExit, createSession, resumeSession, @@ -2231,10 +2281,8 @@ export class CopilotClient { try { const info = await rpc.host.start({ hostId, - hostname, - port, - token, - requireConnectionToken, + localServer, + githubEnvironment, sessionFactory: createSession ? true : undefined, resumeFactory: resumeSession ? true : undefined, }); @@ -3467,8 +3515,16 @@ export class CopilotClient { const connection = this.connection; const messageWriter = this.messageWriter; + const cliProcess = this.isExternalServer ? null : this.cliProcess; + const connectionEpoch = this.connectionEpoch; + const isCurrentConnection = () => + connectionEpoch === this.connectionEpoch && + this.connection === connection && + (cliProcess === null || this.cliProcess === cliProcess); + let disconnecting = false; + let exitFallback: ReturnType | undefined; const markDisconnected = () => { - if (this.connection !== connection) { + if (!isCurrentConnection()) { connection.dispose(); return; } @@ -3482,15 +3538,54 @@ export class CopilotClient { this.githubTokenProviders.clear(); this.requestAdapter?.cancelPending(); connection.dispose(); + this.socket?.destroy(); + this.connection = null; + this.messageWriter = null; + this._rpc = null; + this._internalRpc = null; + this.modelsCache = null; }; - this.connection.onClose(() => { + const disconnectAfterDrain = () => { + if (exitFallback !== undefined) { + clearTimeout(exitFallback); + exitFallback = undefined; + } + if (!isCurrentConnection()) { + connection.dispose(); + return; + } + if (disconnecting) return; + disconnecting = true; + this.connectionClosed = true; if (messageWriter) messageWriter.suppressWriteErrors = true; // jsonrpc dispatches parsed messages asynchronously, one per event-loop // turn. Drain them before clearing callbacks and rejecting unanswered RPCs. void connection.drain().then(markDisconnected); - }); + }; + this.connection.onClose(disconnectAfterDrain); + // Descendants can retain inherited output pipes after the runtime exits. + // Observe the owned process without waiting for those pipes to reach EOF. + if (cliProcess) { + cliProcess.once("exit", () => { + if (!isCurrentConnection()) { + connection.dispose(); + return; + } + if (disconnecting) return; + if (this.socket) { + disconnectAfterDrain(); + return; + } + this.connectionClosed = true; + if (messageWriter) messageWriter.suppressWriteErrors = true; + // Reader EOF is authoritative for messages still buffered after process exit. + // A descendant can keep stdout open, so bound the fallback. + exitFallback = setTimeout(disconnectAfterDrain, RUNTIME_SHUTDOWN_TIMEOUT_MS); + exitFallback.unref(); + }); + } this.connection.onError(() => { - if (this.connection === connection) { + if (connectionEpoch === this.connectionEpoch && this.connection === connection) { this.state = "disconnected"; } }); diff --git a/nodejs/src/ffiRuntimeHost.ts b/nodejs/src/ffiRuntimeHost.ts index f2db8eb163..947417871a 100644 --- a/nodejs/src/ffiRuntimeHost.ts +++ b/nodejs/src/ffiRuntimeHost.ts @@ -243,7 +243,7 @@ export class FfiRuntimeHost { ); if (!this.connectionId) { this.unregisterCallback(); - this.lib.hostShutdown(this.serverId); + await this.shutdownHost(); this.serverId = 0; throw new Error("copilot_runtime_connection_open failed."); } @@ -256,7 +256,7 @@ export class FfiRuntimeHost { } finally { this.starting = false; if (this.disposed) { - void this.tryFinalizeCleanup(); + await this.tryFinalizeCleanup(); } } } @@ -305,6 +305,19 @@ export class FfiRuntimeHost { } } + private shutdownHost(): Promise { + // Native shutdown waits for probe reaping; keep the JS event loop free while it joins. + return new Promise((resolvePromise, rejectPromise) => { + this.lib.hostShutdown.async(this.serverId, (error: Error | null, result: boolean) => { + if (error) { + rejectPromise(error); + } else { + resolvePromise(result); + } + }); + }); + } + private unregisterCallback(): boolean { if (this.outboundCallback === undefined) { return true; @@ -387,7 +400,7 @@ export class FfiRuntimeHost { if (this.serverId) { try { - if (!this.lib.hostShutdown(this.serverId)) { + if (!(await this.shutdownHost())) { console.error( `In-process FFI host shutdown did not recognize server ${this.serverId}.` ); diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 3aa1574748..4511e89f22 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -31,6 +31,7 @@ export type AuthInfo = | TokenProviderAuthInfo | CopilotApiTokenAuthInfo | UserAuthInfo + | AccountAuthInfo | GhCliAuthInfo | ApiKeyAuthInfo; /** @@ -416,6 +417,8 @@ export type AuthInfoType = | "env" /** Authentication from an interactive user sign-in. */ | "user" + /** Authentication from a selected provider-owned account, without a GitHub credential. */ + | "account" /** Authentication delegated to the GitHub CLI. */ | "gh-cli" /** Authentication from an API key credential. */ @@ -484,17 +487,19 @@ export type AuthLoginStep = kind: "error"; }; /** - * Terminal disposition of a login persistence attempt. + * Disposition of a login attempt, including pending user decisions. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "AuthLoginResultStatus". */ /** @experimental */ export type AuthLoginResultStatus = - /** The credential was persisted and the account is signed in. */ + /** The credential was persisted and the selected account is signed in. */ | "completed" /** Persistence needs explicit consent to store the token in plaintext. */ | "needs-plaintext-consent" + /** Credentials are saved; select an account using a returned selectionId as advance input to complete sign-in. */ + | "needs-account-selection" /** The user declined plaintext persistence. */ | "declined"; /** @@ -1436,6 +1441,14 @@ export type ConnectorMcpStatus = | "disabled" /** The Connector currently has no live server configuration. */ | "not_configured"; +/** + * Session account selection, or null. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorSessionAccountResult". + */ +/** @experimental */ +export type ConnectorSessionAccountResult = ConnectorSessionAccount | null; /** * Closed set of public task kinds a connection can negotiate. * @@ -1464,6 +1477,46 @@ export type ContentFilterMode = | "markdown" /** Remove characters that can hide directives. */ | "hidden_characters"; +/** + * Component of session customization discovery. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CustomizationReloadSubsystem". + */ +/** @experimental */ +export type 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" + | (string & {}); +/** + * Result of reloading a customization component. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CustomizationReloadStatus". + */ +/** @experimental */ +export type 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" + | (string & {}); /** * Source category for a collected debug bundle entry. * @@ -1772,59 +1825,63 @@ export type DiscoveredMcpServerType = /** Server is backed by an in-memory runtime implementation. */ | "memory"; /** - * Indicates whether the command was accepted into the local execution queue. + * Kind of component that supplied a provider adapter or row. Attribution does not confer authority. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "EnqueueCommandResult". + * via the `definition` "ModelProviderProvenanceSource". */ /** @experimental */ -export type EnqueueCommandResult = AcceptedEnqueueCommandResult | UnsupportedEnqueueCommandResult; +export type ModelProviderProvenanceSource = + /** Built into the runtime. */ + | "builtIn" + /** Derived from existing user configuration. */ + | "configured" + /** Supplied by an extension. */ + | "extension" + /** Supplied by another trusted contributor. */ + | "custom"; /** - * How far OneAuth may go to acquire the requested token. + * Typed outcome for a provider operation. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "EntraTokenInteraction". + * via the `definition` "ModelProviderOperationOutcomeCode". */ /** @experimental */ -export type EntraTokenInteraction = - /** Acquire the token without any user interaction, failing if interaction would be required. */ - | "silent" - /** Allow interactive acquisition, prompting the user only when a cached or silent token is unavailable. */ - | "interactive" - /** Always prompt interactively, bypassing any cached or silently-refreshable token. */ - | "force-interactive"; +export type ModelProviderOperationOutcomeCode = + /** The operation completed successfully; an empty inventory is valid. */ + | "success" + /** The provider or instance is absent during discovery, status, or model listing. Distinct from a successful empty inventory. */ + | "absent" + /** The provider is configured or expected but could not be reached. */ + | "unreachable" + /** The operation failed for a reason other than absence or reachability. */ + | "failed"; /** - * Result of a OneAuth token acquisition. + * Indicates whether the command was accepted into the local execution queue. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "EntraTokenAcquireResult". + * via the `definition` "EnqueueCommandResult". */ /** @experimental */ -export type EntraTokenAcquireResult = - | { - /** - * Opaque access token. - */ - accessToken: string; - /** - * Expiry as milliseconds since Unix epoch, when supplied by OneAuth. - */ - expiresOnTimestamp?: number; - /** - * Opaque OneAuth account id, when supplied by the broker. - */ - accountId?: string; - /** - * OneAuth token acquisition outcome discriminator. - */ - status: "ok"; - } - | { - /** - * OneAuth token acquisition outcome discriminator. - */ - status: "interaction-required"; - }; +export type EnqueueCommandResult = AcceptedEnqueueCommandResult | UnsupportedEnqueueCommandResult; +/** + * GitHub Mission Control compute kind. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "EnvironmentKind". + */ +/** @experimental */ +export type EnvironmentKind = + /** A user-managed environment on a local machine. */ + | "user-local" + /** A user-managed environment in a GitHub Codespace. */ + | "user-codespace" + /** A GitHub-managed environment backed by GitHub Actions. */ + | "managed-actions" + /** A GitHub-managed sandbox environment. */ + | "managed-sandbox" + /** A GitHub-managed cloud coding agent environment. */ + | "managed-cca"; /** * Either '*' to receive all event types, or a non-empty list of event types to receive * @@ -2608,6 +2665,32 @@ export type SessionLogLevel = | "warning" /** Error message describing a failure. */ | "error"; +/** + * A channel accepted by managedSettings.compose. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ManagedSettingsChannel". + */ +/** @experimental */ +export type ManagedSettingsChannel = + /** Device policy, the strongest channel. */ + | "device" + /** Account or organization policy. */ + | "server" + /** Session-local helper output, the weakest channel. */ + | "policyHelper"; +/** + * Severity of a managed-settings validation finding. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ManagedSettingsDiagnosticSeverity". + */ +/** @experimental */ +export type ManagedSettingsDiagnosticSeverity = + /** The runtime rejects the document. */ + | "error" + /** The runtime accepts the document but ignores the flagged content. */ + | "warning"; /** * UI theme preference per SEP-1865 * @@ -3395,6 +3478,18 @@ export type McpPlanInstallResult = | CatalogUnavailableTransportError | CatalogNotInstallableError | CatalogUnavailableError; +/** + * The sender role of an MCP prompt message. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpPromptRole". + */ +/** @experimental */ +export type McpPromptRole = + /** A message from the user. */ + | "user" + /** A message from the assistant. */ + | "assistant"; /** * MCP server configuration (stdio, remote HTTP/SSE, or in-process) * @@ -3709,17 +3804,49 @@ export type ModelListRequest = skipCache?: boolean; }; /** - * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelSwitchAutoTierStatus". + * via the `definition` "ModelProviderAutomaticDiscoveryMode". */ /** @experimental */ -export type 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`. */ - | "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. */ - | "pending"; +export type ModelProviderAutomaticDiscoveryMode = + /** The adapter declares that automatic discovery is safe when the other policy fields are satisfied. */ + | "automatic" + /** The adapter may refresh instances the user already configured, but must not scan for new instances automatically. */ + | "configuredOnly" + /** The adapter must run only after an explicit user action. */ + | "explicit"; +/** + * Network reach an adapter may use during discovery. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderDiscoveryNetworkScope". + */ +/** @experimental */ +export type ModelProviderDiscoveryNetworkScope = + /** Discovery does not contact a network service. */ + | "none" + /** Discovery is limited to loopback addresses on the local machine. */ + | "loopbackOnly" + /** Discovery contacts only endpoints the user already configured. */ + | "configuredEndpointOnly" + /** Discovery may scan or contact the local network. */ + | "localNetwork" + /** Discovery may contact remote internet services. */ + | "internet"; +/** + * Whether a planned configuration entry is new or already present in the session registry. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderConfigurationDisposition". + */ +/** @experimental */ +export type ModelProviderConfigurationDisposition = + /** No matching entry is registered; the caller should add the entry. */ + | "create" + /** An equivalent entry is already registered; the caller should reuse it rather than adding a duplicate. */ + | "alreadyConfigured"; /** * Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. * @@ -3758,6 +3885,122 @@ export type ProviderConfigTransport = | "http" /** WebSocket transport. */ | "websockets"; +/** + * The product serving the model, reported in telemetry as `model_provider`. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderConfigModelProvider". + */ +/** @experimental */ +export type ProviderConfigModelProvider = + /** OpenAI API. */ + | "openai" + /** Anthropic API. */ + | "anthropic" + /** Azure OpenAI Service. */ + | "azure_openai" + /** Ollama. */ + | "ollama" + /** LM Studio. */ + | "lm_studio" + /** Foundry Local. */ + | "foundry_local" + /** llama.cpp server. */ + | "llama_cpp"; + +/** @experimental */ +export type ProtocolSystemMessageConfig = + | ProtocolSystemMessageAppendConfig + | ProtocolSystemMessageReplaceConfig + | ProtocolSystemMessageCustomizeConfig; + +/** @experimental */ +export type ProtocolAppendMode = "append"; + +/** @experimental */ +export type ProtocolReplaceMode = "replace"; + +/** @experimental */ +export type ProtocolCustomizeMode = "customize"; + +/** @experimental */ +export type ProtocolSectionOverride = (ProtocolStaticSectionOverride | ProtocolMarkerSectionOverride) | undefined; + +/** @experimental */ +export type ProtocolStaticSectionAction = + /** Replace the section content. */ + | "replace" + /** Remove the section content. */ + | "remove" + /** Append content to the section. */ + | "append" + /** Prepend content to the section. */ + | "prepend"; + +/** @experimental */ +export type ProtocolMarkerSectionOverride = + | { + /** + * Section override action discriminator. + */ + action: "transform"; + } + | { + /** + * Section override action discriminator. + */ + action: "preserve"; + }; +/** + * Provider family. Matches the `type` field of a BYOK provider config. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderEndpointType". + */ +/** @experimental */ +export type ProviderEndpointType = + /** OpenAI-compatible endpoint (use the OpenAI client library). */ + | "openai" + /** Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). */ + | "azure" + /** Anthropic endpoint (use the Anthropic client library). */ + | "anthropic"; +/** + * Wire API to be used, when required for the provider type. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderEndpointWireApi". + */ +/** @experimental */ +export type ProviderEndpointWireApi = + /** Classic chat-completions request shape. */ + | "completions" + /** Newer responses request shape. */ + | "responses"; +/** + * Transport to be used for provider requests. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderEndpointTransport". + */ +/** @experimental */ +export type ProviderEndpointTransport = + /** HTTP request/streaming transport. */ + | "http" + /** WebSocket transport. */ + | "websockets"; +/** + * Whether the requested preference was already effective or was accepted for later transactional activation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelSwitchAutoTierStatus". + */ +/** @experimental */ +export type 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`. */ + | "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. */ + | "pending"; /** * Allowed values for the `OptionsUpdateAdditionalContentExclusionPolicyScope` enumeration. * @@ -4066,12 +4309,6 @@ export type PluginsReloadRequest = */ deferRepoHooks?: boolean; }; - -/** @experimental */ -export type ProtocolAppendMode = "append"; - -/** @experimental */ -export type ProtocolCustomizeMode = "customize"; /** * Controls whether the runtime may defer loading an external tool definition. * @@ -4084,82 +4321,6 @@ export type ProtocolExternalToolDefer = | "auto" /** The runtime must include the tool without deferring it. */ | "never"; - -/** @experimental */ -export type ProtocolMarkerSectionOverride = - | { - /** - * Section override action discriminator. - */ - action: "transform"; - } - | { - /** - * Section override action discriminator. - */ - action: "preserve"; - }; - -/** @experimental */ -export type ProtocolReplaceMode = "replace"; - -/** @experimental */ -export type ProtocolSectionOverride = ProtocolStaticSectionOverride | ProtocolMarkerSectionOverride; - -/** @experimental */ -export type ProtocolStaticSectionAction = - /** Replace the section content. */ - | "replace" - /** Remove the section content. */ - | "remove" - /** Append content to the section. */ - | "append" - /** Prepend content to the section. */ - | "prepend"; - -/** @experimental */ -export type ProtocolSystemMessageConfig = - | ProtocolSystemMessageAppendConfig - | ProtocolSystemMessageReplaceConfig - | ProtocolSystemMessageCustomizeConfig; -/** - * Provider family. Matches the `type` field of a BYOK provider config. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ProviderEndpointType". - */ -/** @experimental */ -export type ProviderEndpointType = - /** OpenAI-compatible endpoint (use the OpenAI client library). */ - | "openai" - /** Azure OpenAI endpoint (use the OpenAI client library with the Azure base URL). */ - | "azure" - /** Anthropic endpoint (use the Anthropic client library). */ - | "anthropic"; -/** - * Wire API to be used, when required for the provider type. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ProviderEndpointWireApi". - */ -/** @experimental */ -export type ProviderEndpointWireApi = - /** Classic chat-completions request shape. */ - | "completions" - /** Newer responses request shape. */ - | "responses"; -/** - * Transport to be used for provider requests. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ProviderEndpointTransport". - */ -/** @experimental */ -export type ProviderEndpointTransport = - /** HTTP request/streaming transport. */ - | "http" - /** WebSocket transport. */ - | "websockets"; /** * Optional model identifier to scope the endpoint snapshot to. * @@ -4345,13 +4506,29 @@ export type SandboxConfigSource = /** A repository policy selected the sandbox state. */ | "repository_policy"; /** - * Extensible identifier of a sandbox policy feature whose availability varies between hosts. A plain string, so an older client decodes a name added by a newer runtime; ignore names you do not recognize. Known values: `network` — sandboxed commands can reach the network (`network.allowOutbound`, on by default); on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns. `network_filtering` — host rules and the sandbox proxy (`network.allowedHosts`, `network.blockedHosts`, `network.proxy`); on Linux this needs the same tooling as `network`; on Windows it needs a version with Process Security Environment 1.1 host-loopback support, and a policy that uses it must also set `network.allowLocalNetwork`, because Windows reaches the local proxy only together with private-network access. `denied_paths` — native enforcement of `filesystem.deniedPaths`; on Windows this needs a version whose sandbox contract reports denied-path support. `shell` — shell commands inside the sandbox: bash on macOS and Linux, PowerShell on Windows; on Windows this needs a version with Process Security Environment 1.1 filesystem enumeration support. + * Extensible identifier of a sandbox policy feature whose availability varies between hosts. A plain string, so an older client decodes a name added by a newer runtime; ignore names you do not recognize. Known values: `network` — sandboxed commands can reach the network (`network.allowOutbound`, on by default); on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns. `network_filtering` — host rules and the sandbox proxy (`network.allowedHosts`, `network.blockedHosts`, `network.proxy`); on Linux this needs the same tooling as `network`; on Windows it needs a version with Process Security Environment 1.1 host-loopback support, and a policy that uses it must also set `network.allowLocalNetwork`, because Windows reaches the local proxy only together with private-network access. `denied_paths` — native enforcement of `filesystem.deniedPaths`; on Windows this needs a version whose sandbox contract reports denied-path support. `shell` — shell commands inside the sandbox: bash on macOS and Linux, PowerShell on Windows. `filesystem_enumeration` — enumerate-only filesystem grants, which PowerShell's drive roots use on Windows; this needs a version with Process Security Environment 1.1 filesystem enumeration support. Without it, sandboxed PowerShell still runs, but `Get-Location` may report the drive root, `Set-Location` may fail, and relative paths may resolve against the drive root; the session also receives a `session.warning` with `warningType` `sandbox`. Other platforms always report it. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "SandboxHostCapabilityName". */ /** @experimental */ export type SandboxHostCapabilityName = string; +/** + * State of the persistent certificate authority of the sandbox credential proxy. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SandboxProxyCaState". + */ +/** @experimental */ +export type SandboxProxyCaState = + /** This platform has no supported OS trust store. The proxy uses a per-process certificate bundle. */ + | "unsupported" + /** OS trust does not include the certificate authority, or none is stored. */ + | "notInstalled" + /** OS trust includes the stored certificate authority. */ + | "installed" + /** The runtime could not read the certificate authority or the OS trust store. */ + | "error"; /** * A session-scoped sandbox transition applied while handling a slash command * @@ -4857,6 +5034,7 @@ export type SettableAuthInfo = | SettableTokenAuthInfo | CopilotApiTokenAuthInfo | UserAuthInfo + | AccountAuthInfo | GhCliAuthInfo | ApiKeyAuthInfo; /** @@ -6601,6 +6779,27 @@ export interface UserAuthInfo { login: string; copilotUser?: CopilotUserResponse; } +/** + * An interactive account whose model provider owns its credentials. It carries no GitHub credential. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AccountAuthInfo". + */ +/** @experimental */ +export interface AccountAuthInfo { + /** + * Provider-owned account authentication. + */ + type: "account"; + /** + * Host coordinate owned by the account's model provider. + */ + host: string; + /** + * Login identifying the provider-owned account. + */ + login: string; +} /** * Authentication-info input variant for GitHub CLI credentials, carrying host, login, and the `gh auth token` value. * @@ -7306,6 +7505,46 @@ export interface AuthIdentity { registrationId?: string; copilotUser?: CopilotUserResponse; } +/** + * Credential-free identity metadata. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AuthIdentityMetadata". + */ +/** @experimental */ +export interface AuthIdentityMetadata { + type: AuthInfoType; + /** + * Identity host. + */ + host: string; + /** + * User login. + */ + login: string; +} +/** + * A credential-free account choice after sign-in. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AuthLoginAccount". + */ +/** @experimental */ +export interface AuthLoginAccount { + /** + * Opaque identifier supplied to the next login step to select this account. + */ + selectionId: string; + /** + * Host coordinate owned by the selected account's provider. + */ + host: string; + /** + * Human-readable login for the account choice. + */ + login: string; + kind: AccountKind; +} /** * Advance an in-flight login flow, optionally fulfilling an input-required step. * @@ -7348,7 +7587,7 @@ export interface AuthLoginBegun { step: AuthLoginStep; } /** - * Terminal result of an interactive login flow. + * Result of an interactive login flow. Pending consent or account selection is not terminal. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "AuthLoginResultDto". @@ -7364,6 +7603,10 @@ export interface AuthLoginResultDto { * Login that was signed in, when completed. */ login?: string; + /** + * Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 first. + */ + accounts?: AuthLoginAccount[]; } /** * Cancel an in-flight login flow. @@ -9466,6 +9709,14 @@ export interface ConnectorCapabilities { * Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. */ opaqueAccountSelection: boolean; + /** + * Whether getAccount is supported. Absence means false. + */ + sessionAccountSelection?: boolean; + /** + * Whether reconcile accepts forceConnectorName. Absence means false. + */ + targetedReconcile?: boolean; /** * Maximum accepted polling attempts for one continuation call. */ @@ -9499,6 +9750,18 @@ export interface ConnectorCatalogEntry { * Untrusted service description, when present. */ description?: string; + /** + * Optional catalog logo. + */ + logo?: string; + /** + * Optional catalog tier. + */ + tier?: string; + /** + * Optional catalog release tag. + */ + releaseTag?: string; status: ConnectorCatalogStatus; /** * Opaque stable runtime IDs currently projected into the session for this Connector. @@ -9644,6 +9907,24 @@ export interface ConnectorReconcileRequest { * When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. */ refreshCatalog?: boolean; + /** + * Optional Connector name to reinitialize. Requires the targetedReconcile capability. + */ + forceConnectorName?: string; +} +/** + * Session account selection. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ConnectorSessionAccount". + */ +/** @experimental */ +export interface ConnectorSessionAccount { + /** + * Opaque session-scoped account selection ID. + */ + accountId: string; + authInfo: AuthIdentityMetadata; } /** * Remote session connection parameters. @@ -9791,6 +10072,10 @@ export interface CurrentModel { * Currently active model identifier */ modelId?: string; + /** + * 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; /** * 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. */ @@ -9845,6 +10130,42 @@ export interface CurrentToolMetadata { */ deferLoading?: boolean; } +/** + * Result of one customization reload component. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CustomizationReloadOutcome". + */ +/** @experimental */ +export interface CustomizationReloadOutcome { + subsystem: CustomizationReloadSubsystem; + status: CustomizationReloadStatus; + /** + * Reason for a skipped component or description of a failure, when available + */ + detail?: string | null; +} +/** + * 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. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "CustomizationsReloadResult". + */ +/** @experimental */ +export interface CustomizationsReloadResult { + /** + * Warnings from skill discovery + */ + warnings: string[]; + /** + * Errors from any component that could not be refreshed + */ + errors: string[]; + /** + * Outcome of each component in reload order; a skipped component was not configured or loaded + */ + outcomes: CustomizationReloadOutcome[]; +} /** * A file included in the session debug bundle. * @@ -10295,6 +10616,230 @@ export interface McpSourcePlugin { */ version?: 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. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "DiscoveredModel". + */ +/** @experimental */ +export interface DiscoveredModel { + /** + * Provider-native model identifier. + */ + id: string; + /** + * Provider-reported display name. + */ + name?: string; + provenance: ModelProviderProvenance; + /** + * Provider-reported artifact digest. + */ + digest?: string; + /** + * Provider-reported last-modified timestamp. + */ + modifiedAt?: string; + /** + * Provider-reported artifact size in bytes. + */ + sizeBytes?: number; + details: ModelArtifactDetails; + capabilities: ModelCapabilities; + /** + * Non-fatal warnings encountered while enriching this model. + */ + warnings: ModelProviderWarning[]; +} +/** + * Attribution for the adapter that produced a provider row. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderProvenance". + */ +/** @experimental */ +export interface ModelProviderProvenance { + /** + * Stable opaque adapter identity from the effective catalog. Treat this as a whole identifier, not a parseable owner or kind. + */ + adapterId: string; + /** + * Descriptive provider family that produced this row; not a routing key. + */ + providerKind: string; + source: ModelProviderProvenanceSource; + /** + * Stable contributor identifier when the adapter has an owner outside the runtime. Independent of the contribution mechanism and not a routing key. + */ + ownerId?: string; + /** + * Human-readable contributor name, not the adapter display name. + */ + ownerDisplayName?: string; +} +/** + * Provider-reported model artifact metadata. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelArtifactDetails". + */ +/** @experimental */ +export interface ModelArtifactDetails { + /** + * Artifact format, such as `gguf`. + */ + format?: string; + /** + * Primary model family. + */ + family?: string; + /** + * Provider-reported model families. + */ + families?: string[]; + /** + * Provider-reported parameter count label. + */ + parameterSize?: string; + /** + * Provider-reported quantization label. + */ + quantization?: string; + /** + * Provider-reported model architecture. + */ + architecture?: string; + /** + * Provider-reported tokenizer. + */ + tokenizer?: string; +} +/** + * Model capabilities and limits + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelCapabilities". + */ +/** @experimental */ +export interface ModelCapabilities { + supports?: ModelCapabilitiesSupports; + limits?: ModelCapabilitiesLimits; +} +/** + * Feature flags indicating what the model supports + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelCapabilitiesSupports". + */ +/** @experimental */ +export interface ModelCapabilitiesSupports { + /** + * Whether the model supports provider-native thinking. Independent of configurable reasoning effort; omission means unknown. + */ + thinking?: boolean; + /** + * Whether this model supports vision/image input + */ + vision?: boolean; + /** + * Whether this model supports canonical tool calling + */ + toolCalls?: boolean; + /** + * Whether this model supports reasoning effort configuration + */ + reasoningEffort?: boolean; + adaptive_thinking?: AdaptiveThinkingSupport; +} +/** + * Token limits for prompts, outputs, and context window + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelCapabilitiesLimits". + */ +/** @experimental */ +export interface ModelCapabilitiesLimits { + /** + * Maximum number of prompt/input tokens + */ + max_prompt_tokens?: number; + /** + * Maximum number of output/completion tokens + */ + max_output_tokens?: number; + /** + * Maximum total context window size in tokens + */ + max_context_window_tokens?: number; + vision?: ModelCapabilitiesLimitsVision; +} +/** + * Vision-specific limits + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelCapabilitiesLimitsVision". + */ +/** @experimental */ +export interface ModelCapabilitiesLimitsVision { + /** + * MIME types the model accepts + */ + supported_media_types: string[]; + /** + * Maximum number of images per prompt + */ + max_prompt_images: number; + /** + * Maximum image size in bytes + */ + max_prompt_image_size: number; +} +/** + * A non-fatal provider observation warning. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderWarning". + */ +/** @experimental */ +export interface ModelProviderWarning { + /** + * Machine-readable warning code. + */ + code: string; + /** + * Human-readable warning message. + */ + message: string; +} +/** + * 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. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "DiscoveredModelList". + */ +/** @experimental */ +export interface DiscoveredModelList { + outcome: ModelProviderOperationOutcome; + /** + * Provider-native models in provider order. + */ + models: DiscoveredModel[]; +} +/** + * Typed provider-operation outcome. Use the code for control flow and the optional message for display. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderOperationOutcome". + */ +/** @experimental */ +export interface ModelProviderOperationOutcome { + code: ModelProviderOperationOutcomeCode; + /** + * Human-readable detail for non-success outcomes. + */ + message?: string; +} /** * Slash-prefixed command string to enqueue for FIFO processing. * @@ -10325,36 +10870,145 @@ export interface UnsupportedEnqueueCommandResult { queueId?: null; } /** - * OneAuth token request supplied by a trusted host application. + * Hosting capabilities and session capacity advertised by an environment. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "EntraTokenAcquireRequest". + * via the `definition` "EnvironmentCapabilities". */ /** @experimental */ -export interface EntraTokenAcquireRequest { +export interface EnvironmentCapabilities { /** - * Public client application id. + * Advertised Agent Host Protocol version. */ - clientId: string; + ahpVersion?: string; /** - * Tenant id or tenant selector, such as common or organizations. + * Feature identifiers advertised by the environment. */ - tenantId: string; + features: string[]; /** - * 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. + * Maximum session capacity, when advertised. */ - redirectUri: string; + maxSessions?: number; /** - * Exact delegated scopes to request. - * - * @minItems 1 + * Current session count, when advertised. + */ + currentSessions?: number; +} +/** + * Identify a user-managed Mission Control environment to delete. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "EnvironmentsDeleteRequest". + */ +/** @experimental */ +export interface EnvironmentsDeleteRequest { + /** + * User-managed environment to delete. GitHub-managed environments cannot be deleted. + */ + environmentId: string; +} +/** + * Acknowledgement that the requested environment was deleted. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "EnvironmentsDeleteResult". + */ +/** @experimental */ +export interface EnvironmentsDeleteResult {} +/** + * Identify a Mission Control environment to retrieve. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "EnvironmentsGetRequest". + */ +/** @experimental */ +export interface EnvironmentsGetRequest { + /** + * Identifier assigned by Mission Control. + */ + environmentId: string; +} +/** + * Safe discovery information for the requested environment. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "EnvironmentsGetResult". + */ +/** @experimental */ +export interface EnvironmentsGetResult { + environment: GitHubEnvironment; +} +/** + * Safe discovery information. Host-side relay bootstrap credentials are never included. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "GitHubEnvironment". + */ +/** @experimental */ +export interface GitHubEnvironment { + /** + * Identifier assigned by Mission Control. + */ + id: string; + /** + * Human-readable environment name. + */ + name: string; + kind: EnvironmentKind; + /** + * Open-ended operational status vocabulary. + */ + status: string; + capabilities?: EnvironmentCapabilities; + /** + * Identifier of the environment owner. + */ + ownerId?: string; + /** + * Owner category reported by Mission Control. + */ + ownerType?: string; + /** + * Organization identifier, when the environment belongs to an organization. + */ + orgId?: string; + /** + * Discovery labels attached to the environment. + */ + labels?: { + [k: string]: string | undefined; + }; + /** + * Timestamp of the last heartbeat received by Mission Control. + */ + lastHeartbeatAt?: string; +} +/** + * Optional discovery filters supported by GitHub Mission Control. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "EnvironmentsListRequest". + */ +/** @experimental */ +export interface EnvironmentsListRequest { + kind?: EnvironmentKind; + /** + * Operational status, such as online, offline, degraded, waking, or draining. */ - scopes: [string, ...string[]]; - interaction: EntraTokenInteraction; + status?: string; +} +/** + * Environments visible to the authenticated caller and matching the supplied filters. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "EnvironmentsListResult". + */ +/** @experimental */ +export interface EnvironmentsListResult { /** - * Previously rejected token that OneAuth must bypass during renewal. + * Safe discovery records without relay bootstrap credentials. */ - accessTokenToRenew?: string; + environments: GitHubEnvironment[]; } /** * Cursor, batch size, and optional long-poll/filter parameters for reading session events. @@ -11536,6 +12190,25 @@ export interface HooksDiscoverResult { */ /** @experimental */ export interface HostConfiguration { + localServer?: HostLocalServerConfiguration; + githubEnvironment?: HostGitHubEnvironmentOptions; + /** + * Whether session materialization is delegated to the owning application. + */ + sessionFactory: boolean; + /** + * Whether app-owned durable sessions are resumed by the owning application. + */ + resumeFactory: boolean; +} +/** + * Normalized local WebSocket listener settings. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostLocalServerConfiguration". + */ +/** @experimental */ +export interface HostLocalServerConfiguration { /** * Hostname or IP address to bind. */ @@ -11552,14 +12225,23 @@ export interface HostConfiguration { * Whether the listener requires token authentication. */ requireConnectionToken: boolean; +} +/** + * GitHub Mission Control registration options. The compute ID is application-owned and stable. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostGitHubEnvironmentOptions". + */ +/** @experimental */ +export interface HostGitHubEnvironmentOptions { /** - * Whether session materialization is delegated to the owning application. + * Human-readable environment display name. */ - sessionFactory?: boolean; + name: string; /** - * Whether app-owned durable sessions are resumed by the owning application. + * Stable application installation identity, reused across host restarts. */ - resumeFactory?: boolean; + computeId: string; } /** * Stops a connection-owned listener and joins its teardown. @@ -11582,6 +12264,31 @@ export interface HostDisposeRequest { */ /** @experimental */ export interface HostEmptyResult {} +/** + * Private credentials delivered only to a runtime-owned Mission Control hosting participant. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostEnvironmentCredentials". + */ +/** @experimental */ +export interface HostEnvironmentCredentials { + /** + * Current bearer token for the authenticated GitHub identity. + */ + token: string; + /** + * Hostname of the authenticated GitHub service. + */ + githubHost: string; + /** + * GitHub API base URL for the authenticated service. + */ + githubApiUrl: string; + /** + * Mission Control API origin for environment registration and management. + */ + missionControlUrl: string; +} /** * Reports a supervised listener's hosting-task termination and cleanup outcome. * @@ -11604,6 +12311,31 @@ export interface HostExitedNotification { */ error?: string | null; } +/** + * Local WebSocket transport options. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostLocalServerOptions". + */ +/** @experimental */ +export interface HostLocalServerOptions { + /** + * Listener hostname. Defaults to 127.0.0.1; explicit non-loopback binds are allowed. + */ + hostname?: string; + /** + * Listener port. Omitted or zero requests an OS-allocated port. + */ + port?: number; + /** + * Nonempty connection token. Generated randomly when required and omitted. + */ + token?: string; + /** + * Require token authentication (default true). Cannot be false with a token. + */ + requireConnectionToken?: boolean; +} /** * Publishes a resident session attached to the listener's owning connection. * @@ -11649,11 +12381,15 @@ export interface HostReadyRequest { /** * Actual bound WebSocket URL. */ - address: string; + address?: string; /** * Configured secret token, absent when authentication is disabled. */ token?: string; + /** + * Registered environment ID, reported only once the relay transport is connected. + */ + environmentId?: string; } /** * Listener-scoped registration, not a copy or durable adoption of a session. @@ -11782,7 +12518,7 @@ export interface HostSessionReleaseRequest { handoffId: string; } /** - * Starts a supervised AHP listener in the runtime's configured working directory. + * Starts a supervised AHP host with at least one explicitly selected transport. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "HostStartRequest". @@ -11793,22 +12529,8 @@ export interface HostStartRequest { * Caller-generated UUID identifying this connection-owned listener. */ hostId: string; - /** - * Listener hostname. Defaults to 127.0.0.1; explicit non-loopback binds are allowed. - */ - hostname?: string; - /** - * Listener port. Omitted or zero requests an OS-allocated port. - */ - port?: number; - /** - * Nonempty connection token. Generated randomly when required and omitted. - */ - token?: string; - /** - * Require token authentication (default true). Cannot be false with a token. - */ - requireConnectionToken?: boolean; + localServer?: HostLocalServerOptions; + githubEnvironment?: HostGitHubEnvironmentOptions; /** * Ask the owning SDK application to materialize AHP sessions. */ @@ -11833,11 +12555,15 @@ export interface HostStartResult { /** * Actual bound WebSocket URL, including the allocated port. */ - url: string; + url?: string; /** * Secret connection token, absent when authentication is disabled. */ token?: string; + /** + * GitHub Mission Control environment ID, present when its relay transport is ready. + */ + environmentId?: string; /** * Separate host process ID, when provided by a legacy runtime. Absent for in-process listeners. */ @@ -13070,6 +13796,130 @@ export interface ManagedMcpServerConfig { */ headersRefreshTtlMs?: number; } +/** + * Lock state and provenance of one managed setting. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ManagedSettingMeta". + */ +/** @experimental */ +export interface ManagedSettingMeta { + /** + * Whether users and repositories may choose a different value. `false` means policy locks the value. + */ + overridable: boolean; + /** + * Channel that supplied this scalar value, matching a `layers[].source`: `device`, `server`, or `policyHelper`. These scalar defaults select one winning channel, not a mixed source. Treat unknown values as additional channels; more may be added. + */ + source: string; +} +/** + * One candidate channel; absent settings represents a channel that delivered no document. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ManagedSettingsComposeLayer". + */ +/** @experimental */ +export interface ManagedSettingsComposeLayer { + source: ManagedSettingsChannel; + /** + * Candidate managed-settings document. Omit when the channel delivered none, as in resolve output. + */ + settings?: JsonValue; +} +/** + * Candidate managed-settings documents to merge without applying them. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ManagedSettingsComposeRequest". + */ +/** @experimental */ +export interface ManagedSettingsComposeRequest { + /** + * One entry per channel. `source` must be `device`, `server`, or `policyHelper`, each at most once (checked at runtime); order does not matter, because channel precedence is fixed. To preview documents from resolve output, map recognized source strings to ManagedSettingsChannel and copy their settings; generated resolve and compose layer types are distinct. Omitted settings means this channel delivered no document. Supplied documents must be valid within the preview limits; warnings are returned in diagnostics. Compose does not reproduce source-failure state or retained enforcement floors from resolve. + */ + layers: ManagedSettingsComposeLayer[]; +} +/** + * The effective managed settings the runtime would enforce for the given documents, in the same shape `managedSettings.resolve` returns. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ManagedSettingsComposeResult". + */ +/** @experimental */ +export interface ManagedSettingsComposeResult { + resolved: ManagedSettingsResolvedData; + values?: ManagedSettingsValues; + meta?: ManagedSettingsMeta; + /** + * Only the supplied channels, strongest first, with canonical documents. Empty canonical documents are represented as absent settings, as in live resolution. + */ + layers: ManagedSettingsLayer[]; + /** + * Warnings about ignored content, with paths prefixed by the channel name. + */ + diagnostics: ManagedSettingsDiagnostic[]; +} +/** + * Typed effective values of managed settings. Each field mirrors the managed-settings schema key of the same name; more keys are added as they are typed. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ManagedSettingsValues". + */ +/** @experimental */ +export interface ManagedSettingsValues { + /** + * Managed default model identifier, as configured. New sessions start with it; it can name a model the account cannot use, so hosts match it against the listed models. + */ + model?: string; + autoTier?: AutoTier; +} +/** + * Per-key lock state and provenance for `ManagedSettingsValues`, with the same field names. Producers emit each typed key in values and meta together; both outer objects are omitted when no typed key is set. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ManagedSettingsMeta". + */ +/** @experimental */ +export interface ManagedSettingsMeta { + model?: ManagedSettingMeta; + autoTier?: ManagedSettingMeta; +} +/** + * One managed-settings channel and the document it delivered. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ManagedSettingsLayer". + */ +/** @experimental */ +export interface ManagedSettingsLayer { + /** + * Channel identifier: `device` (MDM, plist, registry, or managed file), `server` (account or organization policy), or `policyHelper` (session-local helper output, supported by compose). Treat unknown output values as additional channels; more may be added. + */ + source: string; + /** + * Validated managed-settings document this channel delivered. Absent when the channel delivered none. + */ + settings?: JsonValue; +} +/** + * One validation finding for a managed-settings document. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ManagedSettingsDiagnostic". + */ +/** @experimental */ +export interface ManagedSettingsDiagnostic { + /** + * Dot-separated path of the offending setting, such as `autoTier.overridable`. Empty for the document as a whole. + */ + path: string; + severity: ManagedSettingsDiagnosticSeverity; + /** + * Human-readable description of the finding. + */ + message: string; +} /** * Validated device-managed settings discovered before a session exists. * @@ -13087,6 +13937,101 @@ export interface ManagedSettingsReadResult { */ errorMessage?: string; } + +/** @experimental */ +export interface ManagedSettingsResolveRequest { + /** + * Opaque account identifier returned by `account.getAllUsers`. When omitted, the current account is used, or device policy only when no account is signed in. + */ + selectionId?: string; + /** + * GitHub token to resolve instead of the current account. The call fails when the token cannot be resolved. + */ + gitHubToken?: string; + /** + * Embedding client identity for server policy requests, as in session creation. Omit for the CLI identity. + */ + clientName?: string; +} +/** + * Effective enterprise managed settings for an account, resolved without a session. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ManagedSettingsResolveResult". + */ +/** @experimental */ +export interface ManagedSettingsResolveResult { + /** + * Printable opaque identity of the account the settings were resolved for, suitable for comparison and storage, not an account selectionId. Absent when no account was available, in which case only device policy is reported. + */ + account?: string; + resolved: ManagedSettingsResolvedData; + values?: ManagedSettingsValues; + meta?: ManagedSettingsMeta; + /** + * Each managed-settings channel consulted, strongest first, with the validated document it delivered before merging. `resolved.settings` is the merged result. More channels may be added over time. + */ + layers: ManagedSettingsLayer[]; + /** + * Warnings about unavailable policy sources or a failed refresh served from cache. A cached response is not proof of a successful live fetch; `resolved.failClosed` separately describes enforcement. + */ + diagnostics: ManagedSettingsDiagnostic[]; +} +/** + * The authoring JSON schema for managed settings recognized by this runtime. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ManagedSettingsSchemaResult". + */ +/** @experimental */ +export interface ManagedSettingsSchemaResult { + /** + * JSON schema (draft 2020-12) with descriptive shared `x-composition` annotations, not a complete runtime composition contract. Model, effortLevel, and contextTier remain coupled; use `managedSettings.compose` for the runtime's effective result. + */ + schema: JsonValue; + /** + * Version of the runtime that owns this schema. + */ + runtimeVersion: string; +} +/** + * A candidate managed-settings document to validate without applying it. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ManagedSettingsValidateRequest". + */ +/** @experimental */ +export interface ManagedSettingsValidateRequest { + /** + * The document to validate: a JSON object, or a string containing the document's JSON text. Preview documents are limited to 1 MiB and 64 levels of nesting, a stricter resource limit than delivered-policy parsing; violations are returned as diagnostics. + */ + content: JsonValue; + /** + * Channel the document is meant for (`device`, `server`, or `policyHelper`). Some keys are only honored in some channels; for example, a `policyHelper` registration is ignored in policy-helper output. When omitted, no channel-specific checks run. + */ + layer?: string; +} +/** + * Result of validating a managed-settings document. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ManagedSettingsValidateResult". + */ +/** @experimental */ +export interface ManagedSettingsValidateResult { + /** + * Whether the runtime would accept the document within the preview resource limits. Always equals whether `settings` is present. An invalid document is rejected as a whole. + */ + valid: boolean; + /** + * Canonical form of the document the runtime would apply, with unrecognized keys removed. Absent when the document is invalid. + */ + settings?: JsonValue; + /** + * Errors that reject the document and warnings about content the runtime ignores. + */ + diagnostics: ManagedSettingsDiagnostic[]; +} /** * Result of registering a new marketplace. * @@ -13883,6 +14828,64 @@ export interface McpConfigUpdateRequest { name: string; config: McpSerializableServerConfig; } +/** + * Effective MCP configuration entry. Configuration enablement is distinct from the optional live observation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpConfiguredServer". + */ +/** @experimental */ +export interface McpConfiguredServer { + /** + * Server name (config key) + */ + name: string; + /** + * Whether this configured server is enabled after session configuration and policy filtering. + */ + enabled: boolean; + source?: McpServerSource; + /** + * Plugin name that provided this server, when source is plugin. + */ + sourcePlugin?: string; + /** + * Plugin version that provided this server, when source is plugin. + */ + sourcePluginVersion?: string; + /** + * Human-readable display name supplied by configuration. + */ + displayName?: string; + live?: McpConfiguredServerState; +} +/** + * Observational state for a matching already materialized MCP server. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpConfiguredServerState". + */ +/** @experimental */ +export interface McpConfiguredServerState { + status: McpServerStatus; + /** + * Observed connection error, when the materialized server failed. + */ + error?: string; +} +/** + * Effective MCP configuration with optional live observations from matching already materialized servers. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpConfiguredServerList". + */ +/** @experimental */ +export interface McpConfiguredServerList { + /** + * Effective configured MCP servers. + */ + servers: McpConfiguredServer[]; +} /** * Credential-free authentication identity used to configure GitHub MCP. * @@ -14458,6 +15461,23 @@ export interface McpOauthCancelLoginResult { */ cancelled: boolean; } +/** + * Host-delivered callback for a runtime-managed MCP OAuth login. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpOauthCompleteRequest". + */ +/** @experimental */ +export interface McpOauthCompleteRequest { + /** + * Opaque identifier returned by session.mcp.oauth.login for the pending external callback. + */ + authorizationId: 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. + */ + callbackUrl: string; +} /** * Pending MCP OAuth request ID and host-provided token or cancellation response. * @@ -14486,7 +15506,7 @@ export interface McpOauthHandlePendingResult { success: boolean; } /** - * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "McpOauthLoginRequest". @@ -14522,6 +15542,10 @@ export interface McpOauthLoginRequest { */ publicClient?: boolean; grantType?: McpOauthLoginGrantType; + /** + * 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; /** * Required for owned login. Consumes the exact prepareLogin handle once. * Set forceReauth and display options during preparation, not consumption. @@ -14546,9 +15570,13 @@ export interface McpOauthLoginResult { loginId?: string; status?: McpOwnedOauthLoginStatus; /** - * 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 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. */ authorizationUrl?: string; + /** + * 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; } /** * Effect-free preparation bound to the existing local session, requester and installation, with frozen options. @@ -14781,6 +15809,235 @@ export interface McpPrepareInstallRequest { source: McpServerCardReference; secretStorage: McpInstallationSecretStorage; } +/** + * An MCP prompt descriptor. Server-provided non-standard fields are exposed under `additionalProperties`. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpPrompt". + */ +/** @experimental */ +export interface McpPrompt { + /** + * The programmatic name of the prompt + */ + name: string; + /** + * Human-readable display title + */ + title?: string; + /** + * Description of what this prompt provides + */ + description?: string; + /** + * Arguments accepted by the prompt + */ + arguments?: McpPromptArgument[]; + /** + * Icons associated with this prompt + */ + icons?: McpPromptIcon[]; + /** + * Prompt-level metadata + */ + _meta?: { + [k: string]: JsonValue | undefined; + }; + /** + * Server-provided non-standard descriptor fields + */ + additionalProperties?: { + [k: string]: JsonValue | undefined; + }; +} +/** + * An argument accepted by an MCP prompt. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpPromptArgument". + */ +/** @experimental */ +export interface McpPromptArgument { + /** + * Name of the argument + */ + name: string; + /** + * Description of the argument + */ + description?: string; + /** + * Whether the argument is required; omission is distinct from false + */ + required?: boolean; + /** + * Argument-level metadata + */ + _meta?: { + [k: string]: JsonValue | undefined; + }; + /** + * Server-provided non-standard argument fields + */ + additionalProperties?: { + [k: string]: JsonValue | undefined; + }; +} +/** + * An MCP prompt icon with standard size hints and preserved non-standard fields. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpPromptIcon". + */ +/** @experimental */ +export interface McpPromptIcon { + /** + * Icon URI + */ + src: string; + /** + * Icon MIME type, when known + */ + mimeType?: string; + /** + * Icon sizes, such as `48x48` or `any` + */ + sizes?: string[]; + /** + * Theme hint for this icon + */ + theme?: string; + /** + * Server-provided non-standard icon fields + */ + additionalProperties?: { + [k: string]: JsonValue | undefined; + }; +} +/** + * An MCP prompt message with opaque JSON content preserved without flattening or content-type filtering. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpPromptMessage". + */ +/** @experimental */ +export interface McpPromptMessage { + role: McpPromptRole; + /** + * The original MCP content block, including nested metadata and unfamiliar content types + */ + content: JsonValue; + /** + * Message-level metadata + */ + _meta?: { + [k: string]: JsonValue | undefined; + }; + /** + * Server-provided non-standard message fields + */ + additionalProperties?: { + [k: string]: JsonValue | undefined; + }; +} +/** + * MCP server, prompt name, and optional string-valued arguments. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpPromptsGetRequest". + */ +/** @experimental */ +export interface McpPromptsGetRequest { + /** + * Name of the MCP server hosting the prompt + */ + serverName: string; + /** + * The programmatic name of the prompt + */ + promptName: string; + /** + * String-valued arguments to pass to the prompt + */ + arguments?: { + [k: string]: string | undefined; + }; +} +/** + * Prompt messages returned by the MCP server without sending them to the model. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpPromptsGetResult". + */ +/** @experimental */ +export interface McpPromptsGetResult { + /** + * Description of the prompt + */ + description?: string; + /** + * Ordered prompt messages + */ + messages: McpPromptMessage[]; + /** + * MCP result metadata + */ + _meta?: { + [k: string]: JsonValue | undefined; + }; + /** + * Server-provided non-standard result fields + */ + additionalProperties?: { + [k: string]: JsonValue | undefined; + }; +} +/** + * MCP server whose prompts to enumerate. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpPromptsListRequest". + */ +/** @experimental */ +export interface McpPromptsListRequest { + /** + * Name of the MCP server whose prompts to enumerate + */ + serverName: string; + /** + * Opaque MCP pagination cursor from a prior `nextCursor` value + */ + cursor?: string; +} +/** + * One page of prompts advertised by the named MCP server. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpPromptsListResult". + */ +/** @experimental */ +export interface McpPromptsListResult { + /** + * Prompts advertised by the server + */ + prompts: McpPrompt[]; + /** + * Opaque cursor for the next page, if the server has more prompts + */ + nextCursor?: string; + /** + * MCP result metadata + */ + _meta?: { + [k: string]: JsonValue | undefined; + }; + /** + * Server-provided non-standard result fields + */ + additionalProperties?: { + [k: string]: JsonValue | undefined; + }; +} /** * Registration parameters for an external MCP client. * @@ -15760,82 +17017,6 @@ export interface Model { warningMessages?: ModelMessage[]; provider?: ModelProviderRef; } -/** - * Model capabilities and limits - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelCapabilities". - */ -/** @experimental */ -export interface ModelCapabilities { - supports?: ModelCapabilitiesSupports; - limits?: ModelCapabilitiesLimits; -} -/** - * Feature flags indicating what the model supports - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelCapabilitiesSupports". - */ -/** @experimental */ -export interface ModelCapabilitiesSupports { - /** - * Whether this model supports vision/image input - */ - vision?: boolean; - /** - * Whether this model supports canonical tool calling - */ - toolCalls?: boolean; - /** - * Whether this model supports reasoning effort configuration - */ - reasoningEffort?: boolean; - adaptive_thinking?: AdaptiveThinkingSupport; -} -/** - * Token limits for prompts, outputs, and context window - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelCapabilitiesLimits". - */ -/** @experimental */ -export interface ModelCapabilitiesLimits { - /** - * Maximum number of prompt/input tokens - */ - max_prompt_tokens?: number; - /** - * Maximum number of output/completion tokens - */ - max_output_tokens?: number; - /** - * Maximum total context window size in tokens - */ - max_context_window_tokens?: number; - vision?: ModelCapabilitiesLimitsVision; -} -/** - * Vision-specific limits - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ModelCapabilitiesLimitsVision". - */ -/** @experimental */ -export interface ModelCapabilitiesLimitsVision { - /** - * MIME types the model accepts - */ - supported_media_types: string[]; - /** - * Maximum number of images per prompt - */ - max_prompt_images: number; - /** - * Maximum image size in bytes - */ - max_prompt_image_size: number; -} /** * Policy state (if applicable) * @@ -16214,6 +17395,285 @@ export interface ModelPickerSettingsContext { */ environment: {}; } +/** + * Normalized model-provider adapter definitions available to the session, not discovered instances. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderAdapterCatalog". + */ +/** @experimental */ +export interface ModelProviderAdapterCatalog { + /** + * Available provider adapters ordered by adapterId. + */ + providers: ModelProviderAdapterDescriptor[]; +} +/** + * A normalized model-provider adapter in the session's effective catalog. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderAdapterDescriptor". + */ +/** @experimental */ +export interface ModelProviderAdapterDescriptor { + /** + * Stable opaque identity for routing to this adapter. Unique in the effective catalog, independent of live registration generations. + */ + adapterId: string; + /** + * Descriptive provider family, such as `ollama`. Different adapters may have the same family; use adapterId for routing. + */ + providerKind: string; + /** + * Human-readable provider name. + */ + displayName: string; + provenance: ModelProviderAttribution; + automaticDiscovery: ModelProviderAutomaticDiscoveryPolicy; + /** + * Operations supported by this provider adapter. + */ + operations: ModelProviderAdapterOperationDescriptor[]; +} +/** + * Contributor attribution, independent of routing identity and authorization. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderAttribution". + */ +/** @experimental */ +export interface ModelProviderAttribution { + source: ModelProviderProvenanceSource; + /** + * Stable contributor identifier. Required and nonblank for extension and custom sources; optional for built-in and configured sources. Does not grant authority. + */ + ownerId?: string; + /** + * Human-readable contributor name, not the adapter display name. + */ + ownerDisplayName?: string; +} +/** + * Adapter-declared policy that tells clients whether discovery may run automatically. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderAutomaticDiscoveryPolicy". + */ +/** @experimental */ +export interface ModelProviderAutomaticDiscoveryPolicy { + mode: ModelProviderAutomaticDiscoveryMode; + networkScope: 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. + */ + requiresInput: boolean; + /** + * True when the adapter must be enabled by a trusted owner, such as a trusted extension, before automatic discovery may run. + */ + requiresTrust: boolean; +} +/** + * An operation supported by a model-provider adapter. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderAdapterOperationDescriptor". + */ +/** @experimental */ +export interface ModelProviderAdapterOperationDescriptor { + /** + * Supported operation name: `discover`, `getStatus`, or `models.list`. Unknown names and duplicate declarations are rejected. + */ + name: string; + /** + * 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?: JsonValue; +} +/** + * 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. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderConfigurationPlan". + */ +/** @experimental */ +export interface ModelProviderConfigurationPlan { + provider: NamedProviderConfig; + model: ProviderModelConfig; + /** + * Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is applied. + */ + selectionId: string; + providerDisposition: ModelProviderConfigurationDisposition; + modelDisposition: ModelProviderConfigurationDisposition; + /** + * Non-fatal warnings carried over from the discovered model, such as capabilities the provider did not report. + */ + warnings: ModelProviderWarning[]; +} +/** + * External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "NamedProviderConfig". + */ +/** @experimental */ +export interface NamedProviderConfig { + /** + * Unique provider name used to qualify model selection IDs. + */ + name: string; + type?: ProviderConfigType; + wireApi?: ProviderConfigWireApi; + transport?: ProviderConfigTransport; + /** + * Base URL for provider API requests. + */ + baseUrl: string; + /** + * Static API key used to authenticate provider requests. + */ + apiKey?: string; + /** + * Static bearer token used to authenticate provider requests. + */ + bearerToken?: string; + azure?: ProviderConfigAzure; + /** + * Additional HTTP headers included with provider requests. + */ + headers?: { + [k: string]: string | undefined; + }; + modelProvider?: ProviderConfigModelProvider; + /** + * Whether the host supplies bearer tokens dynamically. + */ + hasBearerTokenProvider?: boolean; +} +/** + * Azure-specific provider options. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderConfigAzure". + */ +/** @experimental */ +export interface ProviderConfigAzure { + /** + * API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. + */ + apiVersion?: string; +} +/** + * A BYOK model definition referencing a named provider. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderModelConfig". + */ +/** @experimental */ +export interface ProviderModelConfig { + /** + * 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`. + */ + id: string; + /** + * Name of the configured provider that serves this model. + */ + provider: string; + /** + * The model name sent to the provider API for inference. Defaults to `id`. + */ + wireModel?: string; + /** + * Well-known base model id used for behavior/capability/config lookup. Defaults to `id`. + */ + modelId?: string; + /** + * Display name for model pickers. Defaults to the provider-qualified selection id (`provider/id`). + */ + name?: string; + /** + * Maximum prompt/input tokens for the model. + */ + maxPromptTokens?: number; + /** + * Maximum context window tokens for the model. + */ + maxContextWindowTokens?: number; + /** + * Maximum output tokens for the model. + */ + maxOutputTokens?: number; + capabilities?: ModelCapabilitiesOverride; + systemMessage?: ProtocolSystemMessageConfig; + /** + * Provider-published model metadata, preserved verbatim as the public Model.metadata object. + */ + metadata?: { + [k: string]: JsonValue | undefined; + }; +} + +/** @experimental */ +export interface ProtocolSystemMessageAppendConfig { + mode?: ProtocolAppendMode; + /** + * Text appended to the standard system prompt. + */ + content?: string; +} + +/** @experimental */ +export interface ProtocolSystemMessageReplaceConfig { + mode: ProtocolReplaceMode; + /** + * Complete replacement system-message text. + */ + content: string; + /** + * Optional structured blocks corresponding to the replacement content. + */ + contentBlocks?: SystemMessageBlock[]; +} + +/** @experimental */ +export interface SystemMessageBlock { + /** + * Text content for this system-message block. + */ + content: string; + /** + * Whether the block is static and may be cached independently of dynamic prompt content. + */ + isStatic?: boolean; + /** + * Whether providers with explicit prompt caching should place a cache breakpoint after this block. + */ + cacheBreakpoint?: boolean; +} + +/** @experimental */ +export interface ProtocolSystemMessageCustomizeConfig { + mode: ProtocolCustomizeMode; + /** + * Named standard-prompt section overrides. + */ + sections?: { + [k: string]: ProtocolSectionOverride | undefined; + }; + /** + * Text appended after the customized sections. + */ + content?: string; +} + +/** @experimental */ +export interface ProtocolStaticSectionOverride { + action: ProtocolStaticSectionAction; + /** + * Optional content used by replace, append, and prepend operations. + */ + content?: string; +} /** * 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. * @@ -16236,6 +17696,134 @@ export interface ModelProviderDescriptor { */ ordering: number; } +/** + * Provider discovery parameters. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderDiscoverRequest". + */ +/** @experimental */ +export interface ModelProviderDiscoverRequest { + /** + * Opaque adapter identity returned by `session.providers.getCatalog`. + */ + adapterId: 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. + */ + input?: JsonValue; +} +/** + * Provider instances found by a discovery operation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderDiscoverResult". + */ +/** @experimental */ +export interface ModelProviderDiscoverResult { + outcome: ModelProviderOperationOutcome; + /** + * Discovered provider instances. Empty when passive default discovery finds no reachable provider. + */ + instances: ModelProviderInstance[]; +} +/** + * A normalized model-provider instance discovered by the runtime. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderInstance". + */ +/** @experimental */ +export interface ModelProviderInstance { + reference: ModelProviderInstanceReference; + /** + * Human-readable instance name. + */ + displayName: string; + provenance: ModelProviderProvenance; + /** + * Inference API endpoint when the provider exposes one separately from its management endpoint. + */ + inferenceEndpoint?: string; + inferenceType?: ProviderEndpointType; + inferenceWireApi?: ProviderEndpointWireApi; + inferenceTransport?: ProviderEndpointTransport; +} +/** + * Serializable reference to a discovered provider instance. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderInstanceReference". + */ +/** @experimental */ +export interface ModelProviderInstanceReference { + /** + * Stable opaque identity of the adapter that owns this reference. Must be present in the target session's effective catalog. + */ + adapterId: string; + /** + * Descriptive provider family. Must match the selected adapter; not a routing key. + */ + providerKind: string; + /** + * Stable instance identifier derived by the provider adapter, such as `ollama:{normalizedEndpoint}`. + */ + 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. + */ + managementEndpoint: string; +} +/** + * Provider status request parameters. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderGetStatusRequest". + */ +/** @experimental */ +export interface ModelProviderGetStatusRequest { + instance: ModelProviderInstanceReference; +} +/** + * Provider model inventory request parameters. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderModelsListRequest". + */ +/** @experimental */ +export interface ModelProviderModelsListRequest { + 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`. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderPrepareConfigurationRequest". + */ +/** @experimental */ +export interface ModelProviderPrepareConfigurationRequest { + instance: ModelProviderInstance; + model: DiscoveredModel; +} +/** + * Current health information for a provider instance. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelProviderStatus". + */ +/** @experimental */ +export interface ModelProviderStatus { + outcome: ModelProviderOperationOutcome; + instance: ModelProviderInstance; + /** + * Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`. + */ + status: string; + /** + * Provider-reported version. + */ + version?: string; +} /** * 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. * @@ -16562,58 +18150,6 @@ export interface MoveMcpLoadingToBackgroundResult { */ movedToBackground: boolean; } -/** - * External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "NamedProviderConfig". - */ -/** @experimental */ -export interface NamedProviderConfig { - /** - * Unique provider name used to qualify model selection IDs. - */ - name: string; - type?: ProviderConfigType; - wireApi?: ProviderConfigWireApi; - transport?: ProviderConfigTransport; - /** - * Base URL for provider API requests. - */ - baseUrl: string; - /** - * Static API key used to authenticate provider requests. - */ - apiKey?: string; - /** - * Static bearer token used to authenticate provider requests. - */ - bearerToken?: string; - azure?: ProviderConfigAzure; - /** - * Additional HTTP headers included with provider requests. - */ - headers?: { - [k: string]: string | undefined; - }; - /** - * Whether the host supplies bearer tokens dynamically. - */ - hasBearerTokenProvider?: boolean; -} -/** - * Azure-specific provider options. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ProviderConfigAzure". - */ -/** @experimental */ -export interface ProviderConfigAzure { - /** - * API version. When set, uses the versioned deployment route. When omitted, uses the GA versionless v1 route. - */ - apiVersion?: string; -} /** * The session's friendly name, or null when not yet set. * @@ -18751,68 +20287,6 @@ export interface ProtocolExternalToolDefinition { [k: string]: JsonValue | undefined; }; } - -/** @experimental */ -export interface ProtocolStaticSectionOverride { - action: ProtocolStaticSectionAction; - /** - * Optional content used by replace, append, and prepend operations. - */ - content?: string; -} - -/** @experimental */ -export interface ProtocolSystemMessageAppendConfig { - mode?: ProtocolAppendMode; - /** - * Text appended to the standard system prompt. - */ - content?: string; -} - -/** @experimental */ -export interface ProtocolSystemMessageReplaceConfig { - mode: ProtocolReplaceMode; - /** - * Complete replacement system-message text. - */ - content: string; - /** - * Optional structured blocks corresponding to the replacement content. - */ - contentBlocks?: SystemMessageBlock[]; -} - -/** @experimental */ -export interface SystemMessageBlock { - /** - * Text content for this system-message block. - */ - content: string; - /** - * Whether the block is static and may be cached independently of dynamic prompt content. - */ - isStatic?: boolean; - /** - * Whether providers with explicit prompt caching should place a cache breakpoint after this block. - */ - cacheBreakpoint?: boolean; -} - -/** @experimental */ -export interface ProtocolSystemMessageCustomizeConfig { - mode: ProtocolCustomizeMode; - /** - * Named standard-prompt section overrides. - */ - sections?: { - [k: string]: ProtocolSectionOverride; - }; - /** - * Text appended after the customized sections. - */ - content?: string; -} /** * BYOK providers and/or models to add to the session's registry at runtime. Both fields are optional; provide providers, models, or both. * @@ -18830,55 +20304,6 @@ export interface ProviderAddRequest { */ models?: ProviderModelConfig[]; } -/** - * A BYOK model definition referencing a named provider. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "ProviderModelConfig". - */ -/** @experimental */ -export interface ProviderModelConfig { - /** - * 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`. - */ - id: string; - /** - * Name of the configured provider that serves this model. - */ - provider: string; - /** - * The model name sent to the provider API for inference. Defaults to `id`. - */ - wireModel?: string; - /** - * Well-known base model id used for behavior/capability/config lookup. Defaults to `id`. - */ - modelId?: string; - /** - * Display name for model pickers. Defaults to the provider-qualified selection id (`provider/id`). - */ - name?: string; - /** - * Maximum prompt/input tokens for the model. - */ - maxPromptTokens?: number; - /** - * Maximum context window tokens for the model. - */ - maxContextWindowTokens?: number; - /** - * Maximum output tokens for the model. - */ - maxOutputTokens?: number; - capabilities?: ModelCapabilitiesOverride; - systemMessage?: ProtocolSystemMessageConfig; - /** - * Provider-published model metadata, preserved verbatim as the public Model.metadata object. - */ - metadata?: { - [k: string]: JsonValue | undefined; - }; -} /** * The selectable model entries synthesized for the models added by this call. * @@ -18925,6 +20350,7 @@ export interface ProviderConfig { * Provider name used for model and telemetry attribution. */ providerName?: string; + modelProvider?: ProviderConfigModelProvider; /** * The model identifier sent to the provider API for inference (the "wire" model), as opposed to modelId which is the well-known base. */ @@ -19072,6 +20498,40 @@ export interface ProviderTokenAcquireResult { */ token: string; } +/** + * Host-managed model selection ids to withdraw from the session's BYOK registry. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderWithdrawRequest". + */ +/** @experimental */ +export interface 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. + */ + models: string[]; +} +/** + * What the withdrawal actually removed from the registry. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ProviderWithdrawResult". + */ +/** @experimental */ +export interface ProviderWithdrawResult { + /** + * Selection ids that were registered and are now withdrawn. Excludes requested ids that were not present. + */ + withdrawn: string[]; + /** + * Providers removed because one of the withdrawn models was the last entry referencing them. A provider that merely has no models is not removed. + */ + providersRemoved: string[]; + /** + * 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. + */ + modelDeselected?: boolean; +} /** * File attachment * @@ -20707,7 +22167,7 @@ export interface SandboxGrantPathForRequestResult { success: boolean; } /** - * Whether this host can run one sandbox policy feature. A session whose effective policy uses an unsupported feature fails each sandboxed command with `reason`. + * Whether this host can run one sandbox policy feature. A session whose effective policy uses an unsupported feature fails each sandboxed command with `reason`, except `filesystem_enumeration`, whose absence degrades sandboxed PowerShell instead of failing it. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "SandboxHostCapability". @@ -20745,6 +22205,47 @@ export interface SandboxHostSupport { */ capabilities: SandboxHostCapability[]; } +/** + * Result of creating the persistent certificate authority of the sandbox credential proxy. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SandboxProxyCaCreateResult". + */ +/** @experimental */ +export interface SandboxProxyCaCreateResult { + /** + * Absolute path of the public certificate of the certificate authority, in PEM format. + */ + certificatePath: 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. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SandboxProxyCaRequest". + */ +/** @experimental */ +export interface SandboxProxyCaRequest { + sandboxConfig?: SandboxConfig; +} +/** + * Status of the persistent certificate authority of the sandbox credential proxy. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SandboxProxyCaStatus". + */ +/** @experimental */ +export interface SandboxProxyCaStatus { + state: SandboxProxyCaState; + /** + * 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; + /** + * 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: boolean; +} /** * Register an absolute-time scheduled prompt. * @@ -21442,6 +22943,10 @@ export interface SessionFsError { * Free-form detail about the error, for logging/diagnostics */ message?: string; + /** + * For failed writeFile requests only: true if the provider changed the target before failing. Omit when unknown or unchanged. + */ + writeChanged?: boolean; } /** * Path to test for existence in the client-provided session filesystem. @@ -21574,6 +23079,37 @@ export interface SessionFsReaddirWithTypesResult { entries: SessionFsReaddirWithTypesEntry[]; error?: SessionFsError; } +/** + * Path of the binary file to read from the client-provided session filesystem. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionFsReadFileBytesRequest". + */ +/** @experimental */ +export interface SessionFsReadFileBytesRequest { + /** + * Target session identifier + */ + sessionId: string; + /** + * Path using SessionFs conventions + */ + path: string; +} +/** + * File bytes as standard base64, or a filesystem error if the read failed. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionFsReadFileBytesResult". + */ +/** @experimental */ +export interface SessionFsReadFileBytesResult { + /** + * Exact file bytes encoded as standard base64 + */ + content: string; + error?: SessionFsError; +} /** * Path of the file to read from the client-provided session filesystem. * @@ -21663,6 +23199,10 @@ export interface SessionFsSetProviderCapabilities { * Whether the provider supports SQLite query/exists operations */ sqlite?: boolean; + /** + * Whether the provider supports binary reads and writes through sessionFs.readFileBytes and sessionFs.writeFileBytes + */ + binary?: boolean; } /** * Initial working directory, session-state path layout, and path conventions used to register the calling SDK client as the session filesystem provider. A registered provider is authoritative for path interpretation and filesystem facts used by workspace permission validation. Paths are interpreted lexically; home-relative paths (`~` and `~/...`) and Windows drive-relative paths such as `C:foo` are unsupported. Until provider-side canonicalization is supported, providers must not expose symlinks inside allowed roots that escape those roots. @@ -21873,6 +23413,31 @@ export interface SessionFsStatResult { birthtime: string; error?: SessionFsError; } +/** + * File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionFsWriteFileBytesRequest". + */ +/** @experimental */ +export interface SessionFsWriteFileBytesRequest { + /** + * Target session identifier + */ + sessionId: string; + /** + * Path using SessionFs conventions + */ + path: string; + /** + * Exact file bytes encoded as standard base64 + */ + content: string; + /** + * Optional POSIX-style mode for newly created files + */ + mode?: number; +} /** * File path, content to write, and optional mode for the client-provided session filesystem. * @@ -24129,7 +25694,7 @@ export interface ShellCancelUserRequestedRequest { requestId: string; } /** - * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "ShellExecRequest". @@ -24150,7 +25715,7 @@ export interface ShellExecRequest { timeout?: number; } /** - * 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. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "ShellExecResult". @@ -24158,7 +25723,7 @@ export interface ShellExecRequest { /** @experimental */ export interface ShellExecResult { /** - * Unique identifier for tracking streamed output + * Identifier usable with shell.kill while the process is running */ processId: string; } @@ -28632,37 +30197,48 @@ export interface HostReleaseSessionRequest { /** @experimental */ export interface HostDisposeResult {} /** - * Normalized listener settings delivered only to the supervised hosting participant. + * Private credentials delivered only to a runtime-owned Mission Control hosting participant. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HostGetConfigurationResult". + * via the `definition` "HostGetEnvironmentCredentialsResult". */ /** @experimental */ -export interface HostGetConfigurationResult { +export interface HostGetEnvironmentCredentialsResult { /** - * Hostname or IP address to bind. + * Current bearer token for the authenticated GitHub identity. */ - hostname: string; + token: string; /** - * Port to bind, with zero requesting OS allocation. + * Hostname of the authenticated GitHub service. */ - port: number; + githubHost: string; /** - * Secret connection token, absent when authentication is disabled. + * GitHub API base URL for the authenticated service. */ - token?: string; + githubApiUrl: string; /** - * Whether the listener requires token authentication. + * Mission Control API origin for environment registration and management. */ - requireConnectionToken: boolean; + missionControlUrl: string; +} +/** + * Normalized listener settings delivered only to the supervised hosting participant. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HostGetConfigurationResult". + */ +/** @experimental */ +export interface HostGetConfigurationResult { + localServer?: HostLocalServerConfiguration; + githubEnvironment?: HostGitHubEnvironmentOptions; /** * Whether session materialization is delegated to the owning application. */ - sessionFactory?: boolean; + sessionFactory: boolean; /** * Whether app-owned durable sessions are resumed by the owning application. */ - resumeFactory?: boolean; + resumeFactory: boolean; } /** * Empty acknowledgement for a completed host lifecycle operation. @@ -29050,6 +30626,36 @@ export interface InstallationsConfirmRequest { /** Create typed server-scoped RPC methods (no session required). */ export function createServerRpc(connection: MessageConnection) { return { + /** @experimental */ + environments: { + /** + * Lists GitHub Mission Control environments visible to the authenticated identity. Does not require a running host and excludes host relay credentials. + * + * @param params Optional discovery filters supported by GitHub Mission Control. + * + * @returns Environments visible to the authenticated caller and matching the supplied filters. + */ + list: async (params: EnvironmentsListRequest): Promise => + connection.sendRequest("environments.list", params), + /** + * Gets safe discovery information for a GitHub Mission Control environment without requiring a running host. + * + * @param params Identify a Mission Control environment to retrieve. + * + * @returns Safe discovery information for the requested environment. + */ + get: async (params: EnvironmentsGetRequest): Promise => + connection.sendRequest("environments.get", params), + /** + * Deletes a user-managed GitHub Mission Control environment. GitHub-managed environments cannot be deleted. Does not stop a running host, which may register again. + * + * @param params Identify a user-managed Mission Control environment to delete. + * + * @returns Acknowledgement that the requested environment was deleted. + */ + delete: async (params: EnvironmentsDeleteRequest): Promise => + connection.sendRequest("environments.delete", params), + }, /** @experimental */ host: { /** @@ -29062,9 +30668,9 @@ export function createServerRpc(connection: MessageConnection) { publishSession: async (params: HostPublishSessionRequest): Promise => connection.sendRequest("host.publishSession", params), /** - * Starts a connection-owned local AHP listener as a supervised SDK participant. + * Starts a connection-owned AHP host with explicit localServer and/or githubEnvironment transports as a supervised SDK participant. * - * @param params Starts a supervised AHP listener in the runtime's configured working directory. + * @param params Starts a supervised AHP host with at least one explicitly selected transport. * * @returns Listener readiness, returned only after binding and the supervised participant's SDK handshake. */ @@ -29131,6 +30737,52 @@ export function createServerRpc(connection: MessageConnection) { */ getHostSupport: async (): Promise => connection.sendRequest("sandbox.getHostSupport", {}), + /** @experimental */ + proxyCa: { + /** + * 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. + * + * @param 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. + */ + getStatus: async (params: SandboxProxyCaRequest): Promise => + connection.sendRequest("sandbox.proxyCa.getStatus", params), + /** + * 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. + * + * @param 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. + */ + create: async (params: SandboxProxyCaRequest): Promise => + connection.sendRequest("sandbox.proxyCa.create", params), + /** + * 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. + * + * @param 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. + */ + rotate: async (params: SandboxProxyCaRequest): Promise => + connection.sendRequest("sandbox.proxyCa.rotate", params), + /** + * 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. + * + * @param 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. + */ + trust: async (params: SandboxProxyCaRequest): Promise => + connection.sendRequest("sandbox.proxyCa.trust", params), + /** + * 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. + * + * @returns Status of the persistent certificate authority of the sandbox credential proxy. + */ + remove: async (): Promise => + connection.sendRequest("sandbox.proxyCa.remove", {}), + }, }, /** @experimental */ tools: { @@ -29714,17 +31366,51 @@ export function createServerRpc(connection: MessageConnection) { /** @experimental */ managedSettings: { /** - * Discovers device-managed settings from production MDM and managed-file sources, validates them against the runtime-owned managed-settings schema, and returns the canonical JSON without requiring a session. + * Discovers device-managed settings from production MDM and managed-file sources, validates them against the runtime-owned managed-settings schema, and returns the canonical JSON without requiring a session. `managedSettings.resolve` returns the same device settings together with the account's server policy. * * @returns Validated device-managed settings discovered before a session exists. */ read: async (): Promise => connection.sendRequest("managedSettings.read", {}), /** - * Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing "sync account policy" should start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed. + * Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing "sync account policy" should call `managedSettings.resolve` or start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed. */ clearCache: async (): Promise => connection.sendRequest("managedSettings.clearCache", {}), + /** + * Resolves the effective enterprise managed settings without a session, from the device channel and, when an account is available, the account's server policy through the same per-account cache sessions use. A cached server policy less than an hour old is used without a fetch; otherwise the policy is fetched, and when the fetch fails a cached policy up to 24 hours old is used instead, unless `forceRemoteSettingsRefresh` requires a live fetch. With no account requested or signed in, it reports device policy only; signing out removes the account's cached policy. It can fetch server policy over the network when the cache is stale, so call it off latency-critical paths such as startup rather than before listing models. The policy helper is not run. `layers` lists each channel's document before merging, and `values` and `meta` carry typed effective values and their lock state for the keys typed so far. + * + * @param params Optional opaque account selection or GitHub token whose managed settings are resolved. + * + * @returns Effective enterprise managed settings for an account, resolved without a session. + */ + resolve: async (params: ManagedSettingsResolveRequest): Promise => + connection.sendRequest("managedSettings.resolve", params), + /** + * Returns the managed-settings authoring JSON schema with descriptive `x-composition` annotations aligned with the shared settings-engine vocabulary. These annotations are not a complete runtime composition contract: model, effortLevel, and contextTier remain coupled. Use `managedSettings.compose` for the runtime's effective result. Performs no I/O. + * + * @returns The authoring JSON schema for managed settings recognized by this runtime. + */ + schema: async (): Promise => + connection.sendRequest("managedSettings.schema", {}), + /** + * Validates a candidate managed-settings document the way the runtime validates delivered policy, without applying it. Reports errors that would reject the document, warnings for content the runtime ignores, and the canonical document it would apply. Document text nested more than 64 levels deep is rejected. Performs no I/O. + * + * @param params A candidate managed-settings document to validate without applying it. + * + * @returns Result of validating a managed-settings document. + */ + validate: async (params: ManagedSettingsValidateRequest): Promise => + connection.sendRequest("managedSettings.validate", params), + /** + * Merges candidate managed-settings documents for the device, server, and policy-helper channels into the effective settings the runtime would enforce on this host, using the same precedence and composition rules as live resolution, without applying them. Like live resolution, a server's advisory sandbox force-enable is declined on a host that cannot run the sandbox. Does not fetch policy or read policy files, but may perform blocking OS or subprocess probes for sandbox support. Preview documents are limited to 1 MiB and 64 levels of nesting. + * + * @param params Candidate managed-settings documents to merge without applying them. + * + * @returns The effective managed settings the runtime would enforce for the given documents, in the same shape `managedSettings.resolve` returns. + */ + compose: async (params: ManagedSettingsComposeRequest): Promise => + connection.sendRequest("managedSettings.compose", params), }, /** @experimental */ runtime: { @@ -30040,6 +31726,13 @@ export function createInternalServerRpc(connection: MessageConnection) { */ releaseSession: async (params: HostReleaseSessionRequest): Promise => connection.sendRequest("host.releaseSession", params), + /** + * Resolves current authenticated credentials and remote-control policy only for the runtime-owned Mission Control hosting participant. + * + * @returns Private credentials delivered only to a runtime-owned Mission Control hosting participant. + */ + getEnvironmentCredentials: async (): Promise => + connection.sendRequest("host.getEnvironmentCredentials", {}), /** * Returns listener settings only to the supervised hosting participant over its SDK connection. * @@ -30130,24 +31823,61 @@ export function createInternalServerRpc(connection: MessageConnection) { configureSessionExtensions: async (params: ConfigureSessionExtensionsParams): Promise => connection.sendRequest("sessions.configureSessionExtensions", params), }, - /** @experimental */ - accounts: { - /** - * 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). - * - * @param params OneAuth token request supplied by a trusted host application. - * - * @returns Result of a OneAuth token acquisition. - */ - acquireEntraToken: async (params: EntraTokenAcquireRequest): Promise => - connection.sendRequest("accounts.acquireEntraToken", params), - }, }; } /** Create typed session-scoped RPC methods. */ export function createSessionRpc(connection: MessageConnection, sessionId: string) { return { + /** @experimental */ + providers: { + /** + * 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. + * + * @returns Normalized model-provider adapter definitions available to the session, not discovered instances. + */ + getCatalog: async (): Promise => + connection.sendRequest("session.providers.getCatalog", { sessionId }), + /** + * Discovers reachable instances using an adapter from this session's effective provider catalog and provider-specific discovery input. + * + * @param params Provider discovery parameters. + * + * @returns Provider instances found by a discovery operation. + */ + discover: async (params: ModelProviderDiscoverRequest): Promise => + connection.sendRequest("session.providers.discover", { ...params, sessionId }), + /** + * Gets current health and version information for a discovered model-provider instance. + * + * @param params Provider status request parameters. + * + * @returns Current health information for a provider instance. + */ + getStatus: async (params: ModelProviderGetStatusRequest): Promise => + connection.sendRequest("session.providers.getStatus", { ...params, sessionId }), + /** @experimental */ + models: { + /** + * Lists models installed or otherwise available from a discovered model-provider instance. + * + * @param 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. + */ + list: async (params: ModelProviderModelsListRequest): Promise => + connection.sendRequest("session.providers.models.list", { ...params, sessionId }), + /** + * 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. + * + * @param 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. + */ + prepareConfiguration: async (params: ModelProviderPrepareConfigurationRequest): Promise => + connection.sendRequest("session.providers.models.prepareConfiguration", { ...params, sessionId }), + }, + }, /** * Suspends the session while preserving persisted state for later resume. * @@ -30165,7 +31895,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @experimental */ send: async (params: SendRequest): Promise => - connection.sendRequest("session.send", { sessionId, ...params }), + connection.sendRequest("session.send", { ...params, sessionId }), /** * Sends zero or more user messages to the session in a single turn and returns their message IDs. All provided messages are appended to the conversation in order, then exactly one agent turn runs over the resulting history. When the list is empty, one turn runs over the existing history with no new user message. Remote-backed (Mission Control) sessions do not support this method and will return an error. * @@ -30176,7 +31906,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @experimental */ sendMessages: async (params: SendMessagesRequest): Promise => - connection.sendRequest("session.sendMessages", { sessionId, ...params }), + connection.sendRequest("session.sendMessages", { ...params, sessionId }), /** @experimental */ sandbox: { /** @@ -30194,7 +31924,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of attempting to disable sandboxing for the current session. */ disableForSession: async (params: SandboxDisableForSessionRequest): Promise => - connection.sendRequest("session.sandbox.disableForSession", { sessionId, ...params }), + connection.sendRequest("session.sandbox.disableForSession", { ...params, sessionId }), /** * Adds the path offered by a pending sandbox escalation permission request's sandboxPathGrant to the session's sandbox policy and approves the request, so the blocked operation re-runs inside the sandbox rather than outside it. The request is rejected unless the exact request is still pending, carries a sandboxPathGrant, and the grant still takes effect under the current managed policy. Does not persist the path; hosts that store sandbox settings save it themselves. * @@ -30203,7 +31933,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of accepting a sandbox path grant. */ grantPathForRequest: async (params: SandboxGrantPathForRequestRequest): Promise => - connection.sendRequest("session.sandbox.grantPathForRequest", { sessionId, ...params }), + connection.sendRequest("session.sandbox.grantPathForRequest", { ...params, sessionId }), }, /** * Aborts the current agent turn. @@ -30215,7 +31945,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @experimental */ abort: async (params: AbortRequest): Promise => - connection.sendRequest("session.abort", { sessionId, ...params }), + connection.sendRequest("session.abort", { ...params, sessionId }), /** * Interrupts the current main agent turn while leaving running background work (subagents, sidekicks, and promoted attached shells) alive. No-op when the main loop is not processing. * @@ -30226,7 +31956,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @experimental */ interruptMainTurn: async (params: InterruptMainTurnRequest): Promise => - connection.sendRequest("session.interruptMainTurn", { sessionId, ...params }), + connection.sendRequest("session.interruptMainTurn", { ...params, sessionId }), /** * Cancels every running background agent (task-registry subagents plus sidekick agents) without interrupting the main agent loop. Promoted attached shells are left running. * @@ -30244,7 +31974,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @experimental */ shutdown: async (params: ShutdownRequest): Promise => - connection.sendRequest("session.shutdown", { sessionId, ...params }), + connection.sendRequest("session.shutdown", { ...params, sessionId }), /** @experimental */ gitHubAuth: { /** @@ -30262,7 +31992,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the credential update succeeded. */ setCredentials: async (params: SessionSetCredentialsParams): Promise => - connection.sendRequest("session.gitHubAuth.setCredentials", { sessionId, ...params }), + connection.sendRequest("session.gitHubAuth.setCredentials", { ...params, sessionId }), }, /** @experimental */ accounts: { @@ -30274,7 +32004,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns The enumerated collection, keyed by the same selector as the query. */ enumerate: async (params: AccountsEnumerateRequest): Promise => - connection.sendRequest("session.accounts.enumerate", { sessionId, ...params }), + connection.sendRequest("session.accounts.enumerate", { ...params, sessionId }), /** * Read one typed accounts datum: the active account, a neutral status summary, or the last authentication errors. * @@ -30283,7 +32013,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns The read result, keyed by the same selector as the query. */ get: async (params: AccountsGetRequest): Promise => - connection.sendRequest("session.accounts.get", { sessionId, ...params }), + connection.sendRequest("session.accounts.get", { ...params, sessionId }), /** * Apply one non-interactive accounts mutation: switch the active account, log an account out, or set credentials from a token. * @@ -30292,7 +32022,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of a non-interactive accounts mutation. */ set: async (params: AccountsSetRequest): Promise => - connection.sendRequest("session.accounts.set", { sessionId, ...params }), + connection.sendRequest("session.accounts.set", { ...params, sessionId }), /** @experimental */ login: { /** @@ -30303,7 +32033,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns A started login flow: its opaque id and first step. */ begin: async (params: AuthLoginBeginRequest): Promise => - connection.sendRequest("session.accounts.login.begin", { sessionId, ...params }), + connection.sendRequest("session.accounts.login.begin", { ...params, sessionId }), /** * Advance an in-flight login flow, optionally fulfilling an input-required step, and return the next step. * @@ -30312,14 +32042,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns 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). */ advance: async (params: AuthLoginAdvanceRequest): Promise => - connection.sendRequest("session.accounts.login.advance", { sessionId, ...params }), + connection.sendRequest("session.accounts.login.advance", { ...params, sessionId }), /** * Cancel an in-flight login flow and release its resources. * * @param params Cancel an in-flight login flow. */ cancel: async (params: AuthLoginCancelRequest): Promise => - connection.sendRequest("session.accounts.login.cancel", { sessionId, ...params }), + connection.sendRequest("session.accounts.login.cancel", { ...params, sessionId }), }, }, /** @experimental */ @@ -30332,7 +32062,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of collecting a session debug bundle. */ collectLogs: async (params: DebugCollectLogsRequest): Promise => - connection.sendRequest("session.debug.collectLogs", { sessionId, ...params }), + connection.sendRequest("session.debug.collectLogs", { ...params, sessionId }), }, /** @experimental */ canvas: { @@ -30358,14 +32088,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Open canvas instance snapshot. */ open: async (params: CanvasOpenRequest): Promise => - connection.sendRequest("session.canvas.open", { sessionId, ...params }), + connection.sendRequest("session.canvas.open", { ...params, sessionId }), /** * Closes an open canvas instance. * * @param params Canvas close parameters. */ close: async (params: CanvasCloseRequest): Promise => - connection.sendRequest("session.canvas.close", { sessionId, ...params }), + connection.sendRequest("session.canvas.close", { ...params, sessionId }), /** @experimental */ action: { /** @@ -30376,7 +32106,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Canvas action invocation result. */ invoke: async (params: CanvasActionInvokeRequest): Promise => - connection.sendRequest("session.canvas.action.invoke", { sessionId, ...params }), + connection.sendRequest("session.canvas.action.invoke", { ...params, sessionId }), }, }, /** @experimental */ @@ -30389,7 +32119,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Complete current or terminal workflow run envelope. */ run: async (params: WorkflowRunRequest): Promise => - connection.sendRequest("session.workflow.run", { sessionId, ...params }), + connection.sendRequest("session.workflow.run", { ...params, sessionId }), /** * Resumes a dynamic workflow run using its persisted name, arguments, journal, and accounting. * @@ -30398,7 +32128,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Resolved persisted workflow identity and resumed run envelope. */ resume: async (params: WorkflowResumeRequest): Promise => - connection.sendRequest("session.workflow.resume", { sessionId, ...params }), + connection.sendRequest("session.workflow.resume", { ...params, sessionId }), /** * Gets the current or settled envelope for a dynamic workflow run. * @@ -30407,7 +32137,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Complete current or terminal workflow run envelope. */ getRun: async (params: WorkflowGetRunRequest): Promise => - connection.sendRequest("session.workflow.getRun", { sessionId, ...params }), + connection.sendRequest("session.workflow.getRun", { ...params, sessionId }), /** * Lists durable dynamic workflow runs for this session in creation order. * @@ -30416,7 +32146,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns A page of workflow runs in durable creation order. */ listRuns: async (params: WorkflowListRunsRequest): Promise => - connection.sendRequest("session.workflow.listRuns", { sessionId, ...params }), + connection.sendRequest("session.workflow.listRuns", { ...params, sessionId }), /** * Gets durable and live observability detail for one dynamic workflow run. * @@ -30425,7 +32155,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Full workflow run observability detail. */ getRunDetail: async (params: WorkflowGetRunRequest): Promise => - connection.sendRequest("session.workflow.getRunDetail", { sessionId, ...params }), + connection.sendRequest("session.workflow.getRunDetail", { ...params, sessionId }), /** * Pages durable progress for one dynamic workflow run. * @@ -30434,7 +32164,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns A bidirectional page of workflow progress. */ getRunProgress: async (params: WorkflowGetRunProgressRequest): Promise => - connection.sendRequest("session.workflow.getRunProgress", { sessionId, ...params }), + connection.sendRequest("session.workflow.getRunProgress", { ...params, sessionId }), /** * Requests cancellation of a dynamic workflow run and returns its run envelope. * @@ -30443,7 +32173,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Complete current or terminal workflow run envelope. */ cancel: async (params: WorkflowCancelRequest): Promise => - connection.sendRequest("session.workflow.cancel", { sessionId, ...params }), + connection.sendRequest("session.workflow.cancel", { ...params, sessionId }), /** * Pauses a running dynamic workflow and returns its settled run envelope. * @@ -30452,7 +32182,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Complete current or terminal workflow run envelope. */ pause: async (params: WorkflowPauseRequest): Promise => - connection.sendRequest("session.workflow.pause", { sessionId, ...params }), + connection.sendRequest("session.workflow.pause", { ...params, sessionId }), /** * Records a batch of ordered dynamic workflow progress lines. * @@ -30461,7 +32191,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Acknowledgement that a workflow request was accepted. */ log: async (params: WorkflowLogRequest): Promise => - connection.sendRequest("session.workflow.log", { sessionId, ...params }), + connection.sendRequest("session.workflow.log", { ...params, sessionId }), /** * Runs one dynamic-workflow-scoped subagent and returns its result. * @@ -30470,7 +32200,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of one workflow-scoped subagent call. */ agent: async (params: WorkflowAgentRequest): Promise => - connection.sendRequest("session.workflow.agent", { sessionId, ...params }), + connection.sendRequest("session.workflow.agent", { ...params, sessionId }), /** @experimental */ journal: { /** @@ -30481,7 +32211,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of reading a workflow journal entry. */ get: async (params: WorkflowJournalGetRequest): Promise => - connection.sendRequest("session.workflow.journal.get", { sessionId, ...params }), + connection.sendRequest("session.workflow.journal.get", { ...params, sessionId }), /** * Stores a memoized dynamic workflow journal entry. * @@ -30490,7 +32220,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Acknowledgement that a workflow request was accepted. */ put: async (params: WorkflowJournalPutRequest): Promise => - connection.sendRequest("session.workflow.journal.put", { sessionId, ...params }), + connection.sendRequest("session.workflow.journal.put", { ...params, sessionId }), }, }, /** @experimental */ @@ -30510,7 +32240,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns The model identifier active on the session after the switch. */ switchTo: async (params: ModelSwitchToRequest): Promise => - connection.sendRequest("session.model.switchTo", { sessionId, ...params }), + connection.sendRequest("session.model.switchTo", { ...params, sessionId }), /** * Requests an Auto preference change without changing the session's selected model. The latest unclaimed request wins; the runtime commits it only after a later prompt using the `auto` model mints a usable model and token pair. A `pending` response confirms that the request was accepted, not that it committed. Observe eventual success through `session.model_change`, failure through the ephemeral `session.auto_tier_switch_failed` event, or current unclaimed state through `session.model.getCurrent`. * @@ -30519,7 +32249,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. */ switchAutoTier: async (params: ModelSwitchAutoTierRequest): Promise => - connection.sendRequest("session.model.switchAutoTier", { sessionId, ...params }), + connection.sendRequest("session.model.switchAutoTier", { ...params, sessionId }), /** * Replaces or clears the host-supplied model allowlist for a running session. * @@ -30528,7 +32258,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns The applied host allowlist and effective session model policy after intersection. */ setAllowedModels: async (params: ModelSetAllowedModelsRequest): Promise => - connection.sendRequest("session.model.setAllowedModels", { sessionId, ...params }), + connection.sendRequest("session.model.setAllowedModels", { ...params, sessionId }), /** * Updates the session's reasoning effort without changing the selected model. * @@ -30537,7 +32267,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns 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. */ setReasoningEffort: async (params: ModelSetReasoningEffortRequest): Promise => - connection.sendRequest("session.model.setReasoningEffort", { sessionId, ...params }), + connection.sendRequest("session.model.setReasoningEffort", { ...params, sessionId }), /** * Lists models available to this session using its own auth and integration context. Connected hosts (CLI TUI, GitHub App) should call this through the session client so remote sessions return the remote CLI's available models rather than the caller's. * @@ -30546,7 +32276,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns The list of models available to this session. */ list: async (params?: SessionModelListRequest): Promise => - connection.sendRequest("session.model.list", { sessionId, ...params }), + connection.sendRequest("session.model.list", { ...params, sessionId }), }, /** @experimental */ mode: { @@ -30565,7 +32295,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Outcome of a session mode change, including any model switch it triggered and follow-up the host must perform. */ set: async (params: ModeSetRequest): Promise => - connection.sendRequest("session.mode.set", { sessionId, ...params }), + connection.sendRequest("session.mode.set", { ...params, sessionId }), }, /** @experimental */ name: { @@ -30582,7 +32312,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params New friendly name to apply to the session. */ set: async (params: NameSetRequest): Promise => - connection.sendRequest("session.name.set", { sessionId, ...params }), + connection.sendRequest("session.name.set", { ...params, sessionId }), /** * Persists an auto-generated session summary as the session's name when no user-set name exists. * @@ -30591,7 +32321,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the auto-generated summary was applied as the session's name. */ setAuto: async (params: NameSetAutoRequest): Promise => - connection.sendRequest("session.name.setAuto", { sessionId, ...params }), + connection.sendRequest("session.name.setAuto", { ...params, sessionId }), }, /** @experimental */ plan: { @@ -30608,7 +32338,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params Replacement contents to write to the session plan file. */ update: async (params: PlanUpdateRequest): Promise => - connection.sendRequest("session.plan.update", { sessionId, ...params }), + connection.sendRequest("session.plan.update", { ...params, sessionId }), /** * Deletes the session plan file from the workspace. */ @@ -30646,7 +32376,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Current workspace metadata for the session, including its absolute filesystem path when available. */ updateMetadata: async (params: WorkspacesUpdateMetadataRequest): Promise => - connection.sendRequest("session.workspaces.updateMetadata", { sessionId, ...params }), + connection.sendRequest("session.workspaces.updateMetadata", { ...params, sessionId }), /** * Ensures a local session workspace exists and returns it. * @@ -30655,7 +32385,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Current workspace metadata for the session, including its absolute filesystem path when available. */ ensure: async (params: WorkspacesEnsureRequest): Promise => - connection.sendRequest("session.workspaces.ensure", { sessionId, ...params }), + connection.sendRequest("session.workspaces.ensure", { ...params, sessionId }), /** * Lists files stored in the session workspace files directory. * @@ -30671,14 +32401,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Contents of the requested workspace file as a UTF-8 string. */ readFile: async (params: WorkspacesReadFileRequest): Promise => - connection.sendRequest("session.workspaces.readFile", { sessionId, ...params }), + connection.sendRequest("session.workspaces.readFile", { ...params, sessionId }), /** * Creates or overwrites a file in the session workspace files directory. * * @param params Relative path and UTF-8 content for the workspace file to create or overwrite. */ createFile: async (params: WorkspacesCreateFileRequest): Promise => - connection.sendRequest("session.workspaces.createFile", { sessionId, ...params }), + connection.sendRequest("session.workspaces.createFile", { ...params, sessionId }), /** * Returns metadata for a file or directory in the session workspace files directory. * @@ -30687,28 +32417,28 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Filesystem metadata for a path in the session workspace files directory. */ statFile: async (params: WorkspacesStatFileRequest): Promise => - connection.sendRequest("session.workspaces.statFile", { sessionId, ...params }), + connection.sendRequest("session.workspaces.statFile", { ...params, sessionId }), /** * Creates a directory in the session workspace files directory. * * @param params Directory to create within the session workspace files directory. */ createDirectory: async (params: WorkspacesCreateDirectoryRequest): Promise => - connection.sendRequest("session.workspaces.createDirectory", { sessionId, ...params }), + connection.sendRequest("session.workspaces.createDirectory", { ...params, sessionId }), /** * Removes a file or directory from the session workspace files directory. * * @param params File or directory to remove from the session workspace files directory. */ removePath: async (params: WorkspacesRemovePathRequest): Promise => - connection.sendRequest("session.workspaces.removePath", { sessionId, ...params }), + connection.sendRequest("session.workspaces.removePath", { ...params, sessionId }), /** * Renames a file or directory within the session workspace files directory. * * @param params Source and destination paths for a rename within the session workspace files directory. */ renamePath: async (params: WorkspacesRenamePathRequest): Promise => - connection.sendRequest("session.workspaces.renamePath", { sessionId, ...params }), + connection.sendRequest("session.workspaces.renamePath", { ...params, sessionId }), /** * Lists workspace checkpoints in chronological order. * @@ -30724,7 +32454,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Checkpoint content as a UTF-8 string, or null when the checkpoint or workspace is missing. */ readCheckpoint: async (params: WorkspacesReadCheckpointRequest): Promise => - connection.sendRequest("session.workspaces.readCheckpoint", { sessionId, ...params }), + connection.sendRequest("session.workspaces.readCheckpoint", { ...params, sessionId }), /** * Adds a compaction summary checkpoint to the local session workspace. * @@ -30733,7 +32463,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Persisted summary metadata and refreshed workspace metadata. */ addSummary: async (params: WorkspacesAddSummaryRequest): Promise => - connection.sendRequest("session.workspaces.addSummary", { sessionId, ...params }), + connection.sendRequest("session.workspaces.addSummary", { ...params, sessionId }), /** * Truncates local workspace compaction summaries after a rollback. * @@ -30742,7 +32472,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Current workspace metadata for the session, including its absolute filesystem path when available. */ truncateSummaries: async (params: WorkspacesTruncateSummariesRequest): Promise => - connection.sendRequest("session.workspaces.truncateSummaries", { sessionId, ...params }), + connection.sendRequest("session.workspaces.truncateSummaries", { ...params, sessionId }), /** * Reads the autopilot objective state file from the local session workspace. * @@ -30758,7 +32488,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of writing the autopilot objective file. */ writeAutopilotObjective: async (params: WorkspacesWriteAutopilotObjectiveRequest): Promise => - connection.sendRequest("session.workspaces.writeAutopilotObjective", { sessionId, ...params }), + connection.sendRequest("session.workspaces.writeAutopilotObjective", { ...params, sessionId }), /** * Deletes the autopilot objective state file from the local session workspace. * @@ -30781,7 +32511,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Descriptor for the saved paste file, or null when the workspace is unavailable. */ saveLargePaste: async (params: WorkspacesSaveLargePasteRequest): Promise => - connection.sendRequest("session.workspaces.saveLargePaste", { sessionId, ...params }), + connection.sendRequest("session.workspaces.saveLargePaste", { ...params, sessionId }), /** * Computes a diff for the session workspace. Never rejects for a busy session: a `session`-mode diff that cannot read the session's file-change captures falls back to an unstaged git diff with `isFallback: true` and reports why in `unavailableReason`. * @@ -30790,7 +32520,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Workspace diff result for the requested mode. */ diff: async (params: WorkspacesDiffRequest): Promise => - connection.sendRequest("session.workspaces.diff", { sessionId, ...params }), + connection.sendRequest("session.workspaces.diff", { ...params, sessionId }), }, /** @experimental */ autopilotObjective: { @@ -30819,7 +32549,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Host-driven completion items for the current composer input. Empty when the host returns no items or does not support completions. */ request: async (params: CompletionsRequestRequest): Promise => - connection.sendRequest("session.completions.request", { sessionId, ...params }), + connection.sendRequest("session.completions.request", { ...params, sessionId }), }, /** @experimental */ instructions: { @@ -30831,19 +32561,21 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin getSources: async (): Promise => connection.sendRequest("session.instructions.getSources", { sessionId }), /** - * 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. + * + * @returns Instruction sources loaded for the session, in merge order. */ - reload: async (): Promise => + reload: async (): Promise => connection.sendRequest("session.instructions.reload", { sessionId }), }, /** @experimental */ customizations: { /** - * 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. * - * @returns Diagnostics from reloading skill definitions, with warnings and errors as separate lists. + * @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. */ - reload: async (): Promise => + reload: async (): Promise => connection.sendRequest("session.customizations.reload", { sessionId }), }, /** @experimental */ @@ -30856,7 +32588,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether fleet mode was successfully activated. */ start: async (params: FleetStartRequest): Promise => - connection.sendRequest("session.fleet.start", { sessionId, ...params }), + connection.sendRequest("session.fleet.start", { ...params, sessionId }), }, /** @experimental */ agent: { @@ -30868,14 +32600,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Agents available to the session. */ list: async (params?: SessionAgentListRequest): Promise => - connection.sendRequest("session.agent.list", { sessionId, ...params }), + connection.sendRequest("session.agent.list", { ...params, sessionId }), /** * Sets an in-memory authored prompt override for an available agent. For built-in agents, this replaces only the static base prompt while preserving runtime-owned dynamic prompt composition and behavior. The special `general-purpose` agent is not overrideable. Overrides are not persisted; resumed and forked sessions start without them, so the host must re-apply them. * * @param params An in-memory authored prompt override for an available agent. */ setPrompt: async (params: AgentSetPromptRequest): Promise => - connection.sendRequest("session.agent.setPrompt", { sessionId, ...params }), + connection.sendRequest("session.agent.setPrompt", { ...params, sessionId }), /** * Gets the currently selected custom agent for the session. * @@ -30891,7 +32623,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns The newly selected custom agent. */ select: async (params: AgentSelectRequest): Promise => - connection.sendRequest("session.agent.select", { sessionId, ...params }), + connection.sendRequest("session.agent.select", { ...params, sessionId }), /** * Clears the selected custom agent and returns the session to the default agent. */ @@ -30915,7 +32647,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Identifier assigned to the newly started background agent task. */ startAgent: async (params: TasksStartAgentRequest): Promise => - connection.sendRequest("session.tasks.startAgent", { sessionId, ...params }), + connection.sendRequest("session.tasks.startAgent", { ...params, sessionId }), /** * Lists background tasks tracked by the session. * @@ -30931,7 +32663,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of registering or reclaiming a client-owned task. */ register: async (params: TasksRegisterRequest): Promise => - connection.sendRequest("session.tasks.register", { sessionId, ...params }), + connection.sendRequest("session.tasks.register", { ...params, sessionId }), /** * Publishes generic progress or a terminal outcome for a client-owned task. * @@ -30940,7 +32672,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of publishing a client-owned task update. */ update: async (params: TasksUpdateRequest): Promise => - connection.sendRequest("session.tasks.update", { sessionId, ...params }), + connection.sendRequest("session.tasks.update", { ...params, sessionId }), /** * Refreshes metadata for any detached background shells the runtime knows about. * @@ -30963,7 +32695,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Progress information for the task, or null when no task with that ID is tracked. */ getProgress: async (params: TasksGetProgressRequest): Promise => - connection.sendRequest("session.tasks.getProgress", { sessionId, ...params }), + connection.sendRequest("session.tasks.getProgress", { ...params, sessionId }), /** * Returns the first sync-waiting task that can currently be promoted to background mode. * @@ -30979,7 +32711,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the task was successfully promoted to background mode. */ promoteToBackground: async (params: TasksPromoteToBackgroundRequest): Promise => - connection.sendRequest("session.tasks.promoteToBackground", { sessionId, ...params }), + connection.sendRequest("session.tasks.promoteToBackground", { ...params, sessionId }), /** * Atomically promotes the first promotable sync-waiting task to background mode and returns it. * @@ -30995,7 +32727,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the background task was successfully cancelled. */ cancel: async (params: TasksCancelRequest): Promise => - connection.sendRequest("session.tasks.cancel", { sessionId, ...params }), + connection.sendRequest("session.tasks.cancel", { ...params, sessionId }), /** * Removes a completed or cancelled background task from tracking. * @@ -31004,7 +32736,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the task was removed. False when the task does not exist or is still running/idle. */ remove: async (params: TasksRemoveRequest): Promise => - connection.sendRequest("session.tasks.remove", { sessionId, ...params }), + connection.sendRequest("session.tasks.remove", { ...params, sessionId }), /** * Sends a message to a background agent task. * @@ -31013,7 +32745,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the message was delivered, with an error message when delivery failed. */ sendMessage: async (params: TasksSendMessageRequest): Promise => - connection.sendRequest("session.tasks.sendMessage", { sessionId, ...params }), + connection.sendRequest("session.tasks.sendMessage", { ...params, sessionId }), }, /** @experimental */ skills: { @@ -31037,14 +32769,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params Name of the skill to enable for the session. */ enable: async (params: SkillsEnableRequest): Promise => - connection.sendRequest("session.skills.enable", { sessionId, ...params }), + connection.sendRequest("session.skills.enable", { ...params, sessionId }), /** * Disables a skill for the session. * * @param params Name of the skill to disable for the session. */ disable: async (params: SkillsDisableRequest): Promise => - connection.sendRequest("session.skills.disable", { sessionId, ...params }), + connection.sendRequest("session.skills.disable", { ...params, sessionId }), /** * Reloads skill definitions for the session. * @@ -31061,12 +32793,19 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin /** @experimental */ mcp: { /** - * 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. * * @returns MCP servers configured for the session, with their connection status and host-level state. */ list: async (): Promise => connection.sendRequest("session.mcp.list", { sessionId }), + /** + * 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. + * + * @returns Effective MCP configuration with optional live observations from matching already materialized servers. + */ + listConfigured: async (): Promise => + connection.sendRequest("session.mcp.listConfigured", { sessionId }), /** * 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. * @@ -31075,21 +32814,21 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Tools exposed by the connected MCP server. Throws when the server is not connected. */ listTools: async (params: McpListToolsRequest): Promise => - connection.sendRequest("session.mcp.listTools", { sessionId, ...params }), + connection.sendRequest("session.mcp.listTools", { ...params, sessionId }), /** * Enables an MCP server for the session. * * @param params Name of the MCP server to enable for the session. */ enable: async (params: McpEnableRequest): Promise => - connection.sendRequest("session.mcp.enable", { sessionId, ...params }), + connection.sendRequest("session.mcp.enable", { ...params, sessionId }), /** * Disables an MCP server for the session. * * @param params Name of the MCP server to disable for the session. */ disable: async (params: McpDisableRequest): Promise => - connection.sendRequest("session.mcp.disable", { sessionId, ...params }), + connection.sendRequest("session.mcp.disable", { ...params, sessionId }), /** * Reloads MCP server connections for the session. */ @@ -31110,7 +32849,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Outcome of an MCP sampling execution: success result, failure error, or cancellation. */ executeSampling: async (params: McpExecuteSamplingParams): Promise => - connection.sendRequest("session.mcp.executeSampling", { sessionId, ...params }), + connection.sendRequest("session.mcp.executeSampling", { ...params, sessionId }), /** * Cancels an in-flight MCP sampling execution by request ID. * @@ -31119,7 +32858,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether an in-flight sampling execution with the given requestId was found and cancelled. */ cancelSamplingExecution: async (params: McpCancelSamplingExecutionParams): Promise => - connection.sendRequest("session.mcp.cancelSamplingExecution", { sessionId, ...params }), + connection.sendRequest("session.mcp.cancelSamplingExecution", { ...params, sessionId }), /** * Sets how environment-variable values supplied to MCP servers are resolved (direct or indirect). * @@ -31128,7 +32867,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Env-value mode recorded on the session after the update. */ setEnvValueMode: async (params: McpSetEnvValueModeParams): Promise => - connection.sendRequest("session.mcp.setEnvValueMode", { sessionId, ...params }), + connection.sendRequest("session.mcp.setEnvValueMode", { ...params, sessionId }), /** * Removes the auto-managed `github` MCP server when present. * @@ -31142,21 +32881,21 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params 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. */ startServer: async (params: McpStartServerRequest): Promise => - connection.sendRequest("session.mcp.startServer", { sessionId, ...params }), + connection.sendRequest("session.mcp.startServer", { ...params, sessionId }), /** * Restarts an individual MCP server on the live session (stops then starts). Omit `config` for a config-free restart-by-name of an already-configured server; supply `config` to restart with a replacement configuration. Session-scoped and ephemeral: does NOT modify persistent user configuration (`mcp.config.*`). * * @param params 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. */ restartServer: async (params: McpRestartServerRequest): Promise => - connection.sendRequest("session.mcp.restartServer", { sessionId, ...params }), + connection.sendRequest("session.mcp.restartServer", { ...params, sessionId }), /** * Stops an individual MCP server on the session's host. * * @param params Server name for an individual MCP server stop. */ stopServer: async (params: McpStopServerRequest): Promise => - connection.sendRequest("session.mcp.stopServer", { sessionId, ...params }), + connection.sendRequest("session.mcp.stopServer", { ...params, sessionId }), /** * Checks whether a named MCP server is currently running on the session's host. * @@ -31165,7 +32904,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Whether the named MCP server is running. */ isServerRunning: async (params: McpIsServerRunningRequest): Promise => - connection.sendRequest("session.mcp.isServerRunning", { sessionId, ...params }), + connection.sendRequest("session.mcp.isServerRunning", { ...params, sessionId }), /** @experimental */ oauth: { /** @@ -31176,14 +32915,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending MCP OAuth response was accepted. */ handlePendingRequest: async (params: McpOauthHandlePendingRequest): Promise => - connection.sendRequest("session.mcp.oauth.handlePendingRequest", { sessionId, ...params }), + connection.sendRequest("session.mcp.oauth.handlePendingRequest", { ...params, sessionId }), /** * Notifies the session that MCP OAuth authentication succeeded and updated credentials were persisted, so cached tool definitions can be refreshed. * * @param params Identifies the MCP server whose persisted OAuth credentials were updated. */ authenticationStateChanged: async (params: McpOauthAuthenticationStateChangedRequest): Promise => - connection.sendRequest("session.mcp.oauth.authenticationStateChanged", { sessionId, ...params }), + connection.sendRequest("session.mcp.oauth.authenticationStateChanged", { ...params, sessionId }), /** * Prepares an inert, expiring owned OAuth login bound to the original session requester and exact installation. Does not activate, connect, read credentials or open a browser. * @@ -31192,16 +32931,23 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns An inert runtime-issued login handle. Preparation alone performs no activation or OAuth work. */ prepareLogin: async (params: SessionMcpOauthPrepareLoginRequest): Promise => - connection.sendRequest("session.mcp.oauth.prepareLogin", { sessionId, ...params }), + connection.sendRequest("session.mcp.oauth.prepareLogin", { ...params, sessionId }), /** * 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. * - * @param params Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback success-page copy, and static OAuth client selection. + * @param params Remote MCP server name and optional overrides controlling reauthentication, 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. */ login: async (params: McpOauthLoginRequest): Promise => - connection.sendRequest("session.mcp.oauth.login", { sessionId, ...params }), + connection.sendRequest("session.mcp.oauth.login", { ...params, sessionId }), + /** + * Completes a runtime-managed MCP OAuth login after the authorization server redirects to a host-managed callback URL. + * + * @param params Host-delivered callback for a runtime-managed MCP OAuth login. + */ + complete: async (params: McpOauthCompleteRequest): Promise => + connection.sendRequest("session.mcp.oauth.complete", { ...params, sessionId }), /** * 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. * @@ -31210,7 +32956,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns 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. */ probe: async (params: McpOauthProbeRequest): Promise => - connection.sendRequest("session.mcp.oauth.probe", { sessionId, ...params }), + connection.sendRequest("session.mcp.oauth.probe", { ...params, sessionId }), /** * Cancels the exact owned OAuth login issued to this original session requester, without clearing shared credentials. * @@ -31219,7 +32965,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Honest terminal cancellation result; persistence or recovery failures remain RPC errors. */ cancelLogin: async (params: SessionMcpOauthCancelLoginRequest): Promise => - connection.sendRequest("session.mcp.oauth.cancelLogin", { sessionId, ...params }), + connection.sendRequest("session.mcp.oauth.cancelLogin", { ...params, sessionId }), /** * Responds to a pending MCP OAuth authorization request by its request id. * @@ -31228,7 +32974,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending MCP OAuth response was accepted. */ respond: async (params: McpOauthRespondRequest): Promise => - connection.sendRequest("session.mcp.oauth.respond", { sessionId, ...params }), + connection.sendRequest("session.mcp.oauth.respond", { ...params, sessionId }), }, /** @experimental */ headers: { @@ -31240,7 +32986,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending MCP headers refresh response was accepted. */ handlePendingHeadersRefreshRequest: async (params: McpHeadersHandlePendingHeadersRefreshRequestRequest): Promise => - connection.sendRequest("session.mcp.headers.handlePendingHeadersRefreshRequest", { sessionId, ...params }), + connection.sendRequest("session.mcp.headers.handlePendingHeadersRefreshRequest", { ...params, sessionId }), }, /** @experimental */ apps: { @@ -31252,7 +32998,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Resource contents returned by the MCP server. */ readResource: async (params: McpAppsReadResourceRequest): Promise => - connection.sendRequest("session.mcp.apps.readResource", { sessionId, ...params }), + connection.sendRequest("session.mcp.apps.readResource", { ...params, sessionId }), /** * List tools that an MCP App view is allowed to call (SEP-1865 visibility filter). Returns tools whose `_meta.ui.visibility` is unset (default `["model","app"]`) or includes `"app"`. * @@ -31261,7 +33007,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns App-callable tools from the named MCP server. */ listTools: async (params: McpAppsListToolsRequest): Promise => - connection.sendRequest("session.mcp.apps.listTools", { sessionId, ...params }), + connection.sendRequest("session.mcp.apps.listTools", { ...params, sessionId }), /** * Call an MCP tool from an MCP App view (SEP-1865). Enforces the visibility check that prevents an app iframe from invoking model-only tools. Returns the standard MCP `CallToolResult`. * @@ -31270,14 +33016,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Standard MCP CallToolResult */ callTool: async (params: McpAppsCallToolRequest): Promise => - connection.sendRequest("session.mcp.apps.callTool", { sessionId, ...params }), + connection.sendRequest("session.mcp.apps.callTool", { ...params, sessionId }), /** * Replace the host context returned to MCP App guests on `ui/initialize`. Hosts use this to advertise theme, locale, or other metadata to the guest UI. * * @param params Host context to advertise to MCP App guests. */ setHostContext: async (params: McpAppsSetHostContextRequest): Promise => - connection.sendRequest("session.mcp.apps.setHostContext", { sessionId, ...params }), + connection.sendRequest("session.mcp.apps.setHostContext", { ...params, sessionId }), /** * Read the current host context advertised to MCP App guests. * @@ -31293,7 +33039,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Diagnostic snapshot of MCP Apps wiring for the named server. */ diagnose: async (params: McpAppsDiagnoseRequest): Promise => - connection.sendRequest("session.mcp.apps.diagnose", { sessionId, ...params }), + connection.sendRequest("session.mcp.apps.diagnose", { ...params, sessionId }), }, /** @experimental */ resources: { @@ -31305,7 +33051,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Resource contents returned by the MCP server. */ read: async (params: McpResourcesReadRequest): Promise => - connection.sendRequest("session.mcp.resources.read", { sessionId, ...params }), + connection.sendRequest("session.mcp.resources.read", { ...params, sessionId }), /** * Enumerate one page of resources a connected MCP server exposes (proxies MCP `resources/list`). Pass `cursor` to continue from a prior result's `nextCursor`. * @@ -31314,7 +33060,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns One page of resources advertised by the named MCP server. */ list: async (params: McpResourcesListRequest): Promise => - connection.sendRequest("session.mcp.resources.list", { sessionId, ...params }), + connection.sendRequest("session.mcp.resources.list", { ...params, sessionId }), /** * Enumerate one page of resource templates a connected MCP server exposes (proxies MCP `resources/templates/list`). Pass `cursor` to continue from a prior result's `nextCursor`. * @@ -31323,7 +33069,28 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns One page of resource templates advertised by the named MCP server. */ listTemplates: async (params: McpResourcesListTemplatesRequest): Promise => - connection.sendRequest("session.mcp.resources.listTemplates", { sessionId, ...params }), + connection.sendRequest("session.mcp.resources.listTemplates", { ...params, sessionId }), + }, + /** @experimental */ + prompts: { + /** + * Enumerate one page of prompts a connected MCP server exposes (proxies MCP `prompts/list`). Pass `cursor` to continue from a prior result's `nextCursor`. + * + * @param params MCP server whose prompts to enumerate. + * + * @returns One page of prompts advertised by the named MCP server. + */ + list: async (params: McpPromptsListRequest): Promise => + connection.sendRequest("session.mcp.prompts.list", { ...params, sessionId }), + /** + * 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. + * + * @param params MCP server, prompt name, and optional string-valued arguments. + * + * @returns Prompt messages returned by the MCP server without sending them to the model. + */ + get: async (params: McpPromptsGetRequest): Promise => + connection.sendRequest("session.mcp.prompts.get", { ...params, sessionId }), }, }, /** @experimental */ @@ -31336,7 +33103,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Per-source session diagnostics configuration. */ configure: async (params: DiagnosticsConfigureRequest): Promise => - connection.sendRequest("session.diagnostics.configure", { sessionId, ...params }), + connection.sendRequest("session.diagnostics.configure", { ...params, sessionId }), /** * Reads a bounded batch of retained session diagnostics for the selected sources. Records are never consumed and each reader advances independently through its opaque cursor. * @@ -31345,7 +33112,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns One cursor-addressed page of retained session diagnostics. */ read: async (params: DiagnosticsReadRequest): Promise => - connection.sendRequest("session.diagnostics.read", { sessionId, ...params }), + connection.sendRequest("session.diagnostics.read", { ...params, sessionId }), }, /** @experimental */ connectors: { @@ -31356,6 +33123,13 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin */ getCapabilities: async (): Promise => connection.sendRequest("session.connectors.getCapabilities", { sessionId }), + /** + * Returns the session account selection, or null. + * + * @returns Session account selection, or null. + */ + getAccount: async (): Promise => + connection.sendRequest("session.connectors.getAccount", { sessionId }), /** * Returns authoritative session Connector state from current availability, pinned account selection, cached catalog, and live MCP projection without performing a Connector service request. * @@ -31371,7 +33145,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Validated Connector catalog snapshot cached by the session. */ list: async (params: ConnectorAccountRequest): Promise => - connection.sendRequest("session.connectors.list", { sessionId, ...params }), + connection.sendRequest("session.connectors.list", { ...params, sessionId }), /** * Refreshes and validates the Connector catalog for the pinned opaque account selection. * @@ -31380,7 +33154,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Validated Connector catalog snapshot cached by the session. */ refresh: async (params: ConnectorAccountRequest): Promise => - connection.sendRequest("session.connectors.refresh", { sessionId, ...params }), + connection.sendRequest("session.connectors.refresh", { ...params, sessionId }), /** * Initiates an idempotent Connector connection request without opening a browser. Returns connected when the service is immediately authoritative, consent_required with a validated URL, or pending with an opaque continuation ID. * @@ -31389,7 +33163,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Typed result of initiating or continuing a Connector connection. */ connect: async (params: ConnectorConnectRequest): Promise => - connection.sendRequest("session.connectors.connect", { sessionId, ...params }), + connection.sendRequest("session.connectors.connect", { ...params, sessionId }), /** * Re-initiates an idempotent Connector connection request without browser or UI effects, with the same typed outcomes as connect. * @@ -31398,7 +33172,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Typed result of initiating or continuing a Connector connection. */ reconnect: async (params: ConnectorConnectRequest): Promise => - connection.sendRequest("session.connectors.reconnect", { sessionId, ...params }), + connection.sendRequest("session.connectors.reconnect", { ...params, sessionId }), /** * Continues a pending Connector connection with caller-supplied attempt, interval, and deadline bounds. The runtime never opens the returned consent URL. * @@ -31407,7 +33181,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Typed result of initiating or continuing a Connector connection. */ continueConnection: async (params: ConnectorContinueRequest): Promise => - connection.sendRequest("session.connectors.continueConnection", { sessionId, ...params }), + connection.sendRequest("session.connectors.continueConnection", { ...params, sessionId }), /** * Disconnects one Connector for the pinned opaque account selection, refreshes the authoritative catalog, and removes its session-owned MCP projection. * @@ -31416,7 +33190,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Authoritative result after disconnect and MCP reconciliation. */ disconnect: async (params: ConnectorConnectRequest): Promise => - connection.sendRequest("session.connectors.disconnect", { sessionId, ...params }), + connection.sendRequest("session.connectors.disconnect", { ...params, sessionId }), /** * Reconciles the authoritative cached or freshly requested Connector catalog into the session Connector MCP projection and returns live status. * @@ -31425,14 +33199,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. */ reconcile: async (params: ConnectorReconcileRequest): Promise => - connection.sendRequest("session.connectors.reconcile", { sessionId, ...params }), + connection.sendRequest("session.connectors.reconcile", { ...params, sessionId }), }, /** @experimental */ managedSettings: { /** * Waits for the live session's in-flight managed-settings application, then returns the retained effective snapshot used by runtime enforcement and by `session.managed_settings_resolved`. It does not perform another account, device, or server resolution, and rejects when resolution has not produced a snapshot. * - * @returns Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. + * @returns Effective enterprise managed settings and contributing channels. Session events report applied policy; sessionless resolve reports an account/device snapshot, and compose reports a non-applying preview of candidate documents. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively. Session-local SDK-client policy is included only in session results. Marked experimental while the managed-settings surface stabilizes. */ get: async (): Promise => connection.sendRequest("session.managedSettings.get", { sessionId }), @@ -31454,14 +33228,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of installing a plugin. */ install: async (params: SessionPluginsInstallRequest): Promise => - connection.sendRequest("session.plugins.install", { sessionId, ...params }), + connection.sendRequest("session.plugins.install", { ...params, sessionId }), /** * Uninstalls a plugin when permitted by the live session's retained managed policy. * * @param params Name (or spec) of the plugin to uninstall. */ uninstall: async (params: PluginsUninstallRequest): Promise => - connection.sendRequest("session.plugins.uninstall", { sessionId, ...params }), + connection.sendRequest("session.plugins.uninstall", { ...params, sessionId }), /** * Updates an installed plugin using the live session's authoritative account, working directory, and retained managed policy. * @@ -31470,21 +33244,21 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of updating a single plugin. */ update: async (params: PluginsUpdateRequest): Promise => - connection.sendRequest("session.plugins.update", { sessionId, ...params }), + connection.sendRequest("session.plugins.update", { ...params, sessionId }), /** * Enables installed plugins when permitted by the live session's retained managed policy. * * @param params Plugin names (or specs) to enable in the session's authoritative working directory. */ enable: async (params: SessionPluginsEnableRequest): Promise => - connection.sendRequest("session.plugins.enable", { sessionId, ...params }), + connection.sendRequest("session.plugins.enable", { ...params, sessionId }), /** * Disables installed plugins when permitted by the live session's retained managed policy. * * @param params Plugin names (or specs) to disable in the session's authoritative working directory. */ disable: async (params: SessionPluginsDisableRequest): Promise => - connection.sendRequest("session.plugins.disable", { sessionId, ...params }), + connection.sendRequest("session.plugins.disable", { ...params, sessionId }), /** @experimental */ marketplaces: { /** @@ -31502,7 +33276,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of registering a new marketplace. */ add: async (params: PluginsMarketplacesAddRequest): Promise => - connection.sendRequest("session.plugins.marketplaces.add", { sessionId, ...params }), + connection.sendRequest("session.plugins.marketplaces.add", { ...params, sessionId }), /** * Removes a marketplace when permitted by the live session's retained managed policy. * @@ -31511,7 +33285,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Outcome of the remove attempt, including dependent-plugin info when applicable. */ remove: async (params: PluginsMarketplacesRemoveRequest): Promise => - connection.sendRequest("session.plugins.marketplaces.remove", { sessionId, ...params }), + connection.sendRequest("session.plugins.marketplaces.remove", { ...params, sessionId }), /** * Browses a marketplace resolved through the live session's working directory and retained managed policy. * @@ -31520,7 +33294,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Plugins advertised by the marketplace. */ browse: async (params: PluginsMarketplacesBrowseRequest): Promise => - connection.sendRequest("session.plugins.marketplaces.browse", { sessionId, ...params }), + connection.sendRequest("session.plugins.marketplaces.browse", { ...params, sessionId }), /** * Refreshes marketplaces resolved through the live session's working directory and retained managed policy. * @@ -31529,7 +33303,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of refreshing one or more marketplace catalogs. */ refresh: async (params?: SessionPluginsMarketplacesRefreshRequest): Promise => - connection.sendRequest("session.plugins.marketplaces.refresh", { sessionId, ...params }), + connection.sendRequest("session.plugins.marketplaces.refresh", { ...params, sessionId }), }, /** * Reloads the session's plugin set, refreshing MCP servers, custom agents, hooks, and skills cache so SDK-driven changes via `server.plugins.*` take effect immediately. @@ -31537,7 +33311,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params Optional flags controlling which side effects the reload performs. */ reload: async (params?: SessionPluginsReloadRequest): Promise => - connection.sendRequest("session.plugins.reload", { sessionId, ...params }), + connection.sendRequest("session.plugins.reload", { ...params, sessionId }), }, /** @experimental */ provider: { @@ -31549,7 +33323,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns A snapshot of the provider endpoint the session is currently configured to talk to. */ getEndpoint: async (params?: SessionProviderGetEndpointRequest): Promise => - connection.sendRequest("session.provider.getEndpoint", { sessionId, ...params }), + connection.sendRequest("session.provider.getEndpoint", { ...params, sessionId }), /** * Adds BYOK providers and/or models to the session's registry at runtime, extending the additive registry built from the session's `providers`/`models` options. Both fields are optional, so a call may add providers only, models only, or both. Within a single call providers are registered before models, so a model may reference a provider added in the same call; across calls a model may reference any provider already registered (from session creation or a prior add). A model whose referenced provider is not registered by the end of the call is rejected. Newly added models become selectable via `model.list` / `model.switchTo` and are inherited by sub-agents spawned afterwards. * @@ -31558,7 +33332,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns The selectable model entries synthesized for the models added by this call. */ add: async (params: ProviderAddRequest): Promise => - connection.sendRequest("session.provider.add", { sessionId, ...params }), + connection.sendRequest("session.provider.add", { ...params, sessionId }), /** * Atomically updates the session's BYOK provider and model registry by applying the supplied snapshot, replacing existing entries, updating models, or removing entries absent from the snapshot. * @@ -31567,7 +33341,16 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns The selectable model entries and selection ids synthesized for the synchronized BYOK models. */ sync: async (params: ProviderSyncRequest): Promise => - connection.sendRequest("session.provider.sync", { sessionId, ...params }), + connection.sendRequest("session.provider.sync", { ...params, sessionId }), + /** + * Withdraws named host-managed models from the session's BYOK registry, leaving every other entry untouched. The scoped counterpart to `provider.sync`: a snapshot can only describe entries the caller knows about, so using it to remove one model silently withdraws rows registered by another source, such as a plugin calling `provider.add` at runtime. Naming what to remove leaves unrelated entries alone. Selection ids that are not registered are ignored, so withdrawal is idempotent. A provider is removed only when one of the withdrawn models was the last entry referencing it; a provider that simply has no models, which is the normal state while its rows are supplied by catalog discovery, is left in place. + * + * @param params Host-managed model selection ids to withdraw from the session's BYOK registry. + * + * @returns What the withdrawal actually removed from the registry. + */ + withdraw: async (params: ProviderWithdrawRequest): Promise => + connection.sendRequest("session.provider.withdraw", { ...params, sessionId }), }, /** @experimental */ options: { @@ -31579,7 +33362,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the session options patch was applied successfully. */ update: async (params: SessionUpdateOptionsParams): Promise => - connection.sendRequest("session.options.update", { sessionId, ...params }), + connection.sendRequest("session.options.update", { ...params, sessionId }), }, /** @experimental */ lsp: { @@ -31589,7 +33372,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params Parameters for (re)loading the merged LSP configuration set. */ initialize: async (params: LspInitializeRequest): Promise => - connection.sendRequest("session.lsp.initialize", { sessionId, ...params }), + connection.sendRequest("session.lsp.initialize", { ...params, sessionId }), }, /** @experimental */ extensions: { @@ -31606,14 +33389,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params Source-qualified extension identifier to enable for the session. */ enable: async (params: ExtensionsEnableRequest): Promise => - connection.sendRequest("session.extensions.enable", { sessionId, ...params }), + connection.sendRequest("session.extensions.enable", { ...params, sessionId }), /** * Disables an extension for the session. * * @param params Source-qualified extension identifier to disable for the session. */ disable: async (params: ExtensionsDisableRequest): Promise => - connection.sendRequest("session.extensions.disable", { sessionId, ...params }), + connection.sendRequest("session.extensions.disable", { ...params, sessionId }), /** * Reloads extension definitions and processes for the session. */ @@ -31625,7 +33408,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params Parameters for session.extensions.sendAttachmentsToMessage. */ sendAttachmentsToMessage: async (params: SendAttachmentsToMessageParams): Promise => - connection.sendRequest("session.extensions.sendAttachmentsToMessage", { sessionId, ...params }), + connection.sendRequest("session.extensions.sendAttachmentsToMessage", { ...params, sessionId }), }, /** @experimental */ tools: { @@ -31637,7 +33420,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Canonical result returned by a session tool. */ execute: async (params: ToolsExecuteRequest): Promise => - connection.sendRequest("session.tools.execute", { sessionId, ...params }), + connection.sendRequest("session.tools.execute", { ...params, sessionId }), /** * Returns the Rust-owned built-in tool descriptors used to construct the session's offered tool set. * @@ -31646,7 +33429,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Rust-owned built-in tool descriptors for the session. */ getBuiltinDescriptors: async (params: ToolsGetBuiltinDescriptorsRequest): Promise => - connection.sendRequest("session.tools.getBuiltinDescriptors", { sessionId, ...params }), + connection.sendRequest("session.tools.getBuiltinDescriptors", { ...params, sessionId }), /** * Projects a completed task_complete tool call into its label-safe session event payload. * @@ -31655,7 +33438,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Task completion notification with summary from the agent */ taskCompleteEventData: async (params: ToolsTaskCompleteEventDataRequest): Promise => - connection.sendRequest("session.tools.taskCompleteEventData", { sessionId, ...params }), + connection.sendRequest("session.tools.taskCompleteEventData", { ...params, sessionId }), /** * Provides the result for a pending external tool call. * @@ -31664,7 +33447,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the external tool call result was handled successfully. */ handlePendingToolCall: async (params: HandlePendingToolCallRequest): Promise => - connection.sendRequest("session.tools.handlePendingToolCall", { sessionId, ...params }), + connection.sendRequest("session.tools.handlePendingToolCall", { ...params, sessionId }), /** * Resolves, builds, and validates the runtime tool list for the session. * @@ -31687,7 +33470,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Empty result after replacing the calling connection's externally implemented tools. */ set: async (params: ToolsSetRequest): Promise => - connection.sendRequest("session.tools.set", { sessionId, ...params }), + connection.sendRequest("session.tools.set", { ...params, sessionId }), /** * Sets the current session's live subagent settings override, which takes precedence over persisted user settings until cleared. Persisted user settings remain the source of truth for future sessions. * @@ -31696,7 +33479,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Empty result after applying subagent settings */ updateSubagentSettings: async (params: UpdateSubagentSettingsRequest): Promise => - connection.sendRequest("session.tools.updateSubagentSettings", { sessionId, ...params }), + connection.sendRequest("session.tools.updateSubagentSettings", { ...params, sessionId }), }, /** @experimental */ commands: { @@ -31708,7 +33491,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Slash commands available in the session, after applying any include/exclude filters. */ list: async (params?: SessionCommandsListRequest): Promise => - connection.sendRequest("session.commands.list", { sessionId, ...params }), + connection.sendRequest("session.commands.list", { ...params, sessionId }), /** * Invokes a slash command in the session. * @@ -31717,7 +33500,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). */ invoke: async (params: CommandsInvokeRequest): Promise => - connection.sendRequest("session.commands.invoke", { sessionId, ...params }), + connection.sendRequest("session.commands.invoke", { ...params, sessionId }), /** * Reports completion of a pending client-handled slash command. * @@ -31726,7 +33509,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending client-handled command was completed successfully. */ handlePendingCommand: async (params: CommandsHandlePendingCommandRequest): Promise => - connection.sendRequest("session.commands.handlePendingCommand", { sessionId, ...params }), + connection.sendRequest("session.commands.handlePendingCommand", { ...params, sessionId }), /** * Executes a slash command synchronously and returns any error. * @@ -31735,7 +33518,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Error message produced while executing the command, if any. */ execute: async (params: ExecuteCommandParams): Promise => - connection.sendRequest("session.commands.execute", { sessionId, ...params }), + connection.sendRequest("session.commands.execute", { ...params, sessionId }), /** * Enqueues a slash command for FIFO processing on the local session. * @@ -31744,7 +33527,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the command was accepted into the local execution queue. */ enqueue: async (params: EnqueueCommandParams): Promise => - connection.sendRequest("session.commands.enqueue", { sessionId, ...params }), + connection.sendRequest("session.commands.enqueue", { ...params, sessionId }), /** * Reports whether the host actually executed a queued command and whether to continue processing. * @@ -31753,7 +33536,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the queued-command response was matched to a pending request. */ respondToQueuedCommand: async (params: CommandsRespondToQueuedCommandRequest): Promise => - connection.sendRequest("session.commands.respondToQueuedCommand", { sessionId, ...params }), + connection.sendRequest("session.commands.respondToQueuedCommand", { ...params, sessionId }), }, /** @experimental */ telemetry: { @@ -31770,7 +33553,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @param params Feature override key/value pairs to attach to subsequent telemetry events from this session. */ setFeatureOverrides: async (params: TelemetrySetFeatureOverridesRequest): Promise => - connection.sendRequest("session.telemetry.setFeatureOverrides", { sessionId, ...params }), + connection.sendRequest("session.telemetry.setFeatureOverrides", { ...params, sessionId }), }, /** @experimental */ ui: { @@ -31782,7 +33565,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Completed transient query. Ordered chunks and the terminal outcome are also delivered through `ui.ephemeral_query` session events while it runs. */ ephemeralQuery: async (params: UIEphemeralQueryRequest): Promise => - connection.sendRequest("session.ui.ephemeralQuery", { sessionId, ...params }), + connection.sendRequest("session.ui.ephemeralQuery", { ...params, sessionId }), /** * Requests structured input from a UI-capable client. * @@ -31791,7 +33574,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns The elicitation response (accept with form values, decline, or cancel) */ elicitation: async (params: UIElicitationRequest): Promise => - connection.sendRequest("session.ui.elicitation", { sessionId, ...params }), + connection.sendRequest("session.ui.elicitation", { ...params, sessionId }), /** * Provides the user response for a pending elicitation request. * @@ -31800,7 +33583,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the elicitation response was accepted; false if it was already resolved by another client. */ handlePendingElicitation: async (params: UIHandlePendingElicitationRequest): Promise => - connection.sendRequest("session.ui.handlePendingElicitation", { sessionId, ...params }), + connection.sendRequest("session.ui.handlePendingElicitation", { ...params, sessionId }), /** * Resolves a pending `user_input.requested` event with the user's response. * @@ -31809,7 +33592,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending UI request was resolved by this call. */ handlePendingUserInput: async (params: UIHandlePendingUserInputRequest): Promise => - connection.sendRequest("session.ui.handlePendingUserInput", { sessionId, ...params }), + connection.sendRequest("session.ui.handlePendingUserInput", { ...params, sessionId }), /** * Resolves a pending `sampling.requested` event with a sampling result, or rejects it. * @@ -31818,7 +33601,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending UI request was resolved by this call. */ handlePendingSampling: async (params: UIHandlePendingSamplingRequest): Promise => - connection.sendRequest("session.ui.handlePendingSampling", { sessionId, ...params }), + connection.sendRequest("session.ui.handlePendingSampling", { ...params, sessionId }), /** * Resolves a pending `auto_mode_switch.requested` event with the user's accept/decline decision. * @@ -31827,7 +33610,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending UI request was resolved by this call. */ handlePendingAutoModeSwitch: async (params: UIHandlePendingAutoModeSwitchRequest): Promise => - connection.sendRequest("session.ui.handlePendingAutoModeSwitch", { sessionId, ...params }), + connection.sendRequest("session.ui.handlePendingAutoModeSwitch", { ...params, sessionId }), /** * Resolves a pending `session_limits_exhausted.requested` event with the user's selected limit action. * @@ -31836,7 +33619,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending UI request was resolved by this call. */ handlePendingSessionLimitsExhausted: async (params: UIHandlePendingSessionLimitsExhaustedRequest): Promise => - connection.sendRequest("session.ui.handlePendingSessionLimitsExhausted", { sessionId, ...params }), + connection.sendRequest("session.ui.handlePendingSessionLimitsExhausted", { ...params, sessionId }), /** * Resolves a pending `exit_plan_mode.requested` event with the user's response. * @@ -31845,7 +33628,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the pending UI request was resolved by this call. */ handlePendingExitPlanMode: async (params: UIHandlePendingExitPlanModeRequest): Promise => - connection.sendRequest("session.ui.handlePendingExitPlanMode", { sessionId, ...params }), + connection.sendRequest("session.ui.handlePendingExitPlanMode", { ...params, sessionId }), /** * Registers an in-process handler for auto-mode-switch requests so the server bridge skips dispatch. * @@ -31861,7 +33644,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the handle was active and the registration count was decremented. */ unregisterDirectAutoModeSwitchHandler: async (params: UIUnregisterDirectAutoModeSwitchHandlerRequest): Promise => - connection.sendRequest("session.ui.unregisterDirectAutoModeSwitchHandler", { sessionId, ...params }), + connection.sendRequest("session.ui.unregisterDirectAutoModeSwitchHandler", { ...params, sessionId }), }, /** @experimental */ permissions: { @@ -31873,7 +33656,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ configure: async (params: PermissionsConfigureParams): Promise => - connection.sendRequest("session.permissions.configure", { sessionId, ...params }), + connection.sendRequest("session.permissions.configure", { ...params, sessionId }), /** * Provides a decision for a pending tool permission request. * @@ -31882,7 +33665,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the permission decision was applied; false when the request was already resolved. */ handlePendingPermissionRequest: async (params: PermissionDecisionRequest): Promise => - connection.sendRequest("session.permissions.handlePendingPermissionRequest", { sessionId, ...params }), + connection.sendRequest("session.permissions.handlePendingPermissionRequest", { ...params, sessionId }), /** * Reconstructs the set of pending tool permission requests from the session's event history. * @@ -31898,7 +33681,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ setApproveAll: async (params: PermissionsSetApproveAllRequest): Promise => - connection.sendRequest("session.permissions.setApproveAll", { sessionId, ...params }), + connection.sendRequest("session.permissions.setApproveAll", { ...params, sessionId }), /** * Sets the permission mode for the session. `manual` follows the normal approval flow, `assisted` attaches LLM safety recommendations, and `allow-all` automatically approves permission requests. The result returns the authoritative post-mutation mode so callers can update local state without racing the `session.permissions_changed` notification. * @@ -31907,7 +33690,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the requested permission mode was applied and reports the authoritative post-mutation mode. */ setMode: async (params: PermissionsSetModeRequest): Promise => - connection.sendRequest("session.permissions.setMode", { sessionId, ...params }), + connection.sendRequest("session.permissions.setMode", { ...params, sessionId }), /** * Returns the current permission mode for the session. * @@ -31923,7 +33706,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ modifyRules: async (params: PermissionsModifyRulesParams): Promise => - connection.sendRequest("session.permissions.modifyRules", { sessionId, ...params }), + connection.sendRequest("session.permissions.modifyRules", { ...params, sessionId }), /** * Sets whether the client wants permission prompts bridged into session events. * @@ -31932,7 +33715,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ setRequired: async (params: PermissionsSetRequiredRequest): Promise => - connection.sendRequest("session.permissions.setRequired", { sessionId, ...params }), + connection.sendRequest("session.permissions.setRequired", { ...params, sessionId }), /** * Clears session-scoped tool approvals and, for full resets, exact session-approved paths. * @@ -31941,7 +33724,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ resetSessionApprovals: async (params: PermissionsResetSessionApprovalsRequest): Promise => - connection.sendRequest("session.permissions.resetSessionApprovals", { sessionId, ...params }), + connection.sendRequest("session.permissions.resetSessionApprovals", { ...params, sessionId }), /** * Notifies the runtime that a permission prompt UI has been shown to the user. * @@ -31950,7 +33733,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ notifyPromptShown: async (params: PermissionPromptShownNotification): Promise => - connection.sendRequest("session.permissions.notifyPromptShown", { sessionId, ...params }), + connection.sendRequest("session.permissions.notifyPromptShown", { ...params, sessionId }), /** @experimental */ paths: { /** @@ -31968,7 +33751,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ add: async (params: PermissionPathsAddParams): Promise => - connection.sendRequest("session.permissions.paths.add", { sessionId, ...params }), + connection.sendRequest("session.permissions.paths.add", { ...params, sessionId }), /** * Updates the session's primary working directory used by the permission policy. * @@ -31977,7 +33760,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ updatePrimary: async (params: PermissionPathsUpdatePrimaryParams): Promise => - connection.sendRequest("session.permissions.paths.updatePrimary", { sessionId, ...params }), + connection.sendRequest("session.permissions.paths.updatePrimary", { ...params, sessionId }), /** * Reports whether a path falls within any of the session's allowed directories. * @@ -31986,7 +33769,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the supplied path is within the session's allowed directories. */ isPathWithinAllowedDirectories: async (params: PermissionPathsAllowedCheckParams): Promise => - connection.sendRequest("session.permissions.paths.isPathWithinAllowedDirectories", { sessionId, ...params }), + connection.sendRequest("session.permissions.paths.isPathWithinAllowedDirectories", { ...params, sessionId }), /** * Reports whether a path falls within the session's workspace (primary) directory. * @@ -31995,7 +33778,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the supplied path is within the session's workspace directory. */ isPathWithinWorkspace: async (params: PermissionPathsWorkspaceCheckParams): Promise => - connection.sendRequest("session.permissions.paths.isPathWithinWorkspace", { sessionId, ...params }), + connection.sendRequest("session.permissions.paths.isPathWithinWorkspace", { ...params, sessionId }), }, /** @experimental */ locations: { @@ -32007,7 +33790,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Resolved location-permissions key and type. */ resolve: async (params: PermissionLocationResolveParams): Promise => - connection.sendRequest("session.permissions.locations.resolve", { sessionId, ...params }), + connection.sendRequest("session.permissions.locations.resolve", { ...params, sessionId }), /** * Applies persisted location-scoped tool approvals and allowed directories for a working directory to this session's permission service. * @@ -32016,7 +33799,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Summary of persisted location permissions applied to the session. */ apply: async (params: PermissionLocationApplyParams): Promise => - connection.sendRequest("session.permissions.locations.apply", { sessionId, ...params }), + connection.sendRequest("session.permissions.locations.apply", { ...params, sessionId }), /** * Persists a tool approval for a permission location and applies its rules to this session's live permission service. * @@ -32025,7 +33808,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ addToolApproval: async (params: PermissionLocationAddToolApprovalParams): Promise => - connection.sendRequest("session.permissions.locations.addToolApproval", { sessionId, ...params }), + connection.sendRequest("session.permissions.locations.addToolApproval", { ...params, sessionId }), }, /** @experimental */ folderTrust: { @@ -32037,7 +33820,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Folder trust check result. */ isTrusted: async (params: FolderTrustCheckParams): Promise => - connection.sendRequest("session.permissions.folderTrust.isTrusted", { sessionId, ...params }), + connection.sendRequest("session.permissions.folderTrust.isTrusted", { ...params, sessionId }), /** * Adds a folder to the user's trusted folders list. * @@ -32046,7 +33829,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ addTrusted: async (params: FolderTrustAddParams): Promise => - connection.sendRequest("session.permissions.folderTrust.addTrusted", { sessionId, ...params }), + connection.sendRequest("session.permissions.folderTrust.addTrusted", { ...params, sessionId }), }, /** @experimental */ urls: { @@ -32058,7 +33841,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ setUnrestrictedMode: async (params: PermissionUrlsSetUnrestrictedModeParams): Promise => - connection.sendRequest("session.permissions.urls.setUnrestrictedMode", { sessionId, ...params }), + connection.sendRequest("session.permissions.urls.setUnrestrictedMode", { ...params, sessionId }), }, }, /** @@ -32071,7 +33854,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @experimental */ log: async (params: LogRequest): Promise => - connection.sendRequest("session.log", { sessionId, ...params }), + connection.sendRequest("session.log", { ...params, sessionId }), /** @experimental */ metadata: { /** @@ -32096,7 +33879,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Client-owned, case-sensitive string metadata persisted with a local session. Clients should namespace keys by owner. Keys must be non-empty and at most 256 UTF-8 bytes; keys under `copilot/` and `github/` are reserved. Values may contain at most 16 KiB of UTF-8 data. A bag may contain at most 128 entries and its serialized sidecar may contain at most 64 KiB. The runtime stores but never interprets these values. */ updateClientMetadata: async (params: MetadataUpdateClientMetadataRequest): Promise => - connection.sendRequest("session.metadata.updateClientMetadata", { sessionId, ...params }), + connection.sendRequest("session.metadata.updateClientMetadata", { ...params, sessionId }), /** * Reports whether the local session is currently processing user/agent messages. * @@ -32119,7 +33902,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Token breakdown for the session's current context window, or null if uninitialized. */ contextInfo: async (params: MetadataContextInfoRequest): Promise => - connection.sendRequest("session.metadata.contextInfo", { sessionId, ...params }), + connection.sendRequest("session.metadata.contextInfo", { ...params, sessionId }), /** * Returns the experimental per-source attribution breakdown of the session's current context window as a flat list of entries (skills, subagents, MCP servers, built-in tools, plugin rollups, system/tool-definition costs, with nesting via parentId), plus the successful compaction count. The heaviest individual messages are available separately via `metadata.getContextHeaviestMessages`. Returns null until the session has initialized its system prompt and tool metadata. * @@ -32135,7 +33918,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns The heaviest individual messages in the session's context window, most-expensive first. */ getContextHeaviestMessages: async (params: MetadataContextHeaviestMessagesRequest): Promise => - connection.sendRequest("session.metadata.getContextHeaviestMessages", { sessionId, ...params }), + connection.sendRequest("session.metadata.getContextHeaviestMessages", { ...params, sessionId }), /** * Records a working-directory/git context change and emits a `session.context_changed` event. 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): a local session's working directory is authoritative and is moved via `metadata.setWorkingDirectory` (or an SDK `session.resume` that supplies a `workingDirectory`), not by this method. * @@ -32144,7 +33927,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns 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. */ recordContextChange: async (params: MetadataRecordContextChangeRequest): Promise => - connection.sendRequest("session.metadata.recordContextChange", { sessionId, ...params }), + connection.sendRequest("session.metadata.recordContextChange", { ...params, sessionId }), /** * Updates the session's working directory. For local sessions the target is validated first (an absolute path that exists on disk) and the permission primary directory is re-based; a rejected validation fails the call before any session state changes. * @@ -32153,7 +33936,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns 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. */ setWorkingDirectory: async (params: MetadataSetWorkingDirectoryRequest): Promise => - connection.sendRequest("session.metadata.setWorkingDirectory", { sessionId, ...params }), + connection.sendRequest("session.metadata.setWorkingDirectory", { ...params, sessionId }), /** * Re-tokenizes the session's existing messages against a model and returns aggregate token totals. * @@ -32162,7 +33945,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns 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. */ recomputeContextTokens: async (params: MetadataRecomputeContextTokensRequest): Promise => - connection.sendRequest("session.metadata.recomputeContextTokens", { sessionId, ...params }), + connection.sendRequest("session.metadata.recomputeContextTokens", { ...params, sessionId }), }, /** @experimental */ contentExclusion: { @@ -32174,19 +33957,19 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Batch content-exclusion result. Callers must fail closed when policy evaluation is unavailable. */ checkPaths: async (params: ContentExclusionCheckPathsRequest): Promise => - connection.sendRequest("session.contentExclusion.checkPaths", { sessionId, ...params }), + connection.sendRequest("session.contentExclusion.checkPaths", { ...params, sessionId }), }, /** @experimental */ shell: { /** - * 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. * - * @param params Shell command to run, with optional working directory and timeout in milliseconds. + * @param 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. + * @returns Identifier of the spawned shell process, usable with shell.kill while the process is running. */ exec: async (params: ShellExecRequest): Promise => - connection.sendRequest("session.shell.exec", { sessionId, ...params }), + connection.sendRequest("session.shell.exec", { ...params, sessionId }), /** * Sends a signal to a shell process previously started via "shell.exec". The signal targets the command's whole process group (POSIX) or job object (Windows), so descendants still in that group are signalled too, not just the direct child. On POSIX a descendant that moved itself into a new session or process group (for example via "setsid") is no longer in the signalled group and survives. * @@ -32195,7 +33978,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the signal was delivered; false if the process was unknown or already exited. */ kill: async (params: ShellKillRequest): Promise => - connection.sendRequest("session.shell.kill", { sessionId, ...params }), + connection.sendRequest("session.shell.kill", { ...params, sessionId }), /** * Executes a user-requested shell command through the session runtime. * @@ -32204,7 +33987,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of a user-requested shell command. */ executeUserRequested: async (params: ShellExecuteUserRequestedRequest): Promise => - connection.sendRequest("session.shell.executeUserRequested", { sessionId, ...params }), + connection.sendRequest("session.shell.executeUserRequested", { ...params, sessionId }), /** * Cancels a user-requested shell command by request ID. * @@ -32213,7 +33996,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Cancellation result for a user-requested shell command. */ cancelUserRequested: async (params: ShellCancelUserRequestedRequest): Promise => - connection.sendRequest("session.shell.cancelUserRequested", { sessionId, ...params }), + connection.sendRequest("session.shell.cancelUserRequested", { ...params, sessionId }), }, /** @experimental */ history: { @@ -32225,7 +34008,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Compaction outcome with the number of tokens and messages removed, summary text, and the resulting context window breakdown. */ compact: async (params?: SessionHistoryCompactRequest): Promise => - connection.sendRequest("session.history.compact", { sessionId, ...params }), + connection.sendRequest("session.history.compact", { ...params, sessionId }), /** * Truncates persisted session history to a specific event. * @@ -32234,7 +34017,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Number of events that were removed by the truncation. */ truncate: async (params: HistoryTruncateRequest): Promise => - connection.sendRequest("session.history.truncate", { sessionId, ...params }), + connection.sendRequest("session.history.truncate", { ...params, sessionId }), /** * Lists the user turns that the session can rewind to. Never rejects for a busy session: rewind reads need the session's file-change captures to be settled, so a session that still holds active work answers with `unavailableReason: "session-busy"` and no points, which the caller can retry. * @@ -32250,7 +34033,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Files and aggregate changes for a prospective rewind. */ previewRewind: async (params: HistoryPreviewRewindRequest): Promise => - connection.sendRequest("session.history.previewRewind", { sessionId, ...params }), + connection.sendRequest("session.history.previewRewind", { ...params, sessionId }), /** * Rewinds the session conversation, optionally restoring files changed by the discarded turns. Not crash-atomic: file restore and conversation truncation are separate stores, applied in that order, so a process crash between them can leave the workspace rewound while the conversation still contains the discarded turns. There is no recovery journal; re-running the same rewind is the recovery path for a crash before truncation lands, since file restore is idempotent (already-restored files are reported as skipped) and truncation is re-derived from the still-retained boundary event. After truncation lands that boundary no longer exists, so the same request is rejected; the only stage that can still be outstanding is snapshot pruning, whose failure leaves orphan snapshots the capture store tolerates. The reverse inconsistency cannot occur, because truncation is never applied before file restore succeeds. * @@ -32259,7 +34042,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Structured outcome of a rewind request. */ rewind: async (params: HistoryRewindRequest): Promise => - connection.sendRequest("session.history.rewind", { sessionId, ...params }), + connection.sendRequest("session.history.rewind", { ...params, sessionId }), /** * Cancels any in-progress background compaction on a local session. * @@ -32289,7 +34072,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns What a successful clear removed. A clear that could not be applied rejects instead of reporting a count. */ clearContext: async (params: HistoryClearContextRequest): Promise => - connection.sendRequest("session.history.clearContext", { sessionId, ...params }), + connection.sendRequest("session.history.clearContext", { ...params, sessionId }), }, /** @experimental */ queue: { @@ -32308,7 +34091,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of moving a queued item. */ moveItem: async (params: QueueMoveItemRequest): Promise => - connection.sendRequest("session.queue.moveItem", { sessionId, ...params }), + connection.sendRequest("session.queue.moveItem", { ...params, sessionId }), /** * Inserts a new queued message at a public visible position. * @@ -32317,7 +34100,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of inserting a queued message. */ insertAt: async (params: QueueInsertAtRequest): Promise => - connection.sendRequest("session.queue.insertAt", { sessionId, ...params }), + connection.sendRequest("session.queue.insertAt", { ...params, sessionId }), /** * Removes an addressable queued item by its stable id. * @@ -32326,7 +34109,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of removing a queued item. */ removeAt: async (params: QueueRemoveAtRequest): Promise => - connection.sendRequest("session.queue.removeAt", { sessionId, ...params }), + connection.sendRequest("session.queue.removeAt", { ...params, sessionId }), /** * Updates the text of an addressable single-message queue item. * @@ -32335,7 +34118,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of editing a queued message. */ updateText: async (params: QueueUpdateTextRequest): Promise => - connection.sendRequest("session.queue.updateText", { sessionId, ...params }), + connection.sendRequest("session.queue.updateText", { ...params, sessionId }), /** * Atomically withdraws an unchanged user message of a local session: from the queued or steering lane while unconsumed, or from the running turn it started while the model has not answered it and nothing the user sent after it is pending. Withdrawing from the running turn interrupts that turn and removes its events from history. A client retaining the original draft may restore it only when removed is true. * @@ -32344,7 +34127,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of withdrawing a user message. */ withdrawMessage: async (params: QueueWithdrawMessageRequest): Promise => - connection.sendRequest("session.queue.withdrawMessage", { sessionId, ...params }), + connection.sendRequest("session.queue.withdrawMessage", { ...params, sessionId }), /** * Atomically appends text and attachments to an unchanged, unconsumed local steering message. Returns updated=false if delivery or withdrawal already claimed the message. * @@ -32353,7 +34136,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of editing a queued message. */ appendSteering: async (params: QueueAppendSteeringRequest): Promise => - connection.sendRequest("session.queue.appendSteering", { sessionId, ...params }), + connection.sendRequest("session.queue.appendSteering", { ...params, sessionId }), /** * Duplicates an addressable queued item immediately after its source. * @@ -32362,14 +34145,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of duplicating a queued item. */ duplicateAt: async (params: QueueDuplicateAtRequest): Promise => - connection.sendRequest("session.queue.duplicateAt", { sessionId, ...params }), + connection.sendRequest("session.queue.duplicateAt", { ...params, sessionId }), /** * Acquires or releases the queued-lane drain pause. * * @param params 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. */ setDrainPaused: async (params: QueueSetDrainPausedRequest): Promise => - connection.sendRequest("session.queue.setDrainPaused", { sessionId, ...params }), + connection.sendRequest("session.queue.setDrainPaused", { ...params, sessionId }), /** * Moves an addressable queued message into the live turn's steering lane. * @@ -32378,7 +34161,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Result of trying to steer a queued message into a live turn. */ sendNow: async (params: QueueSendNowRequest): Promise => - connection.sendRequest("session.queue.sendNow", { sessionId, ...params }), + connection.sendRequest("session.queue.sendNow", { ...params, sessionId }), /** * Removes the most recently queued user-facing item (LIFO). * @@ -32402,7 +34185,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Batch of session events returned by a read, with cursor and continuation metadata. */ read: async (params: EventLogReadRequest): Promise => - connection.sendRequest("session.eventLog.read", { sessionId, ...params }), + connection.sendRequest("session.eventLog.read", { ...params, sessionId }), /** * Returns a snapshot of the current tail cursor without consuming events. * @@ -32418,7 +34201,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Opaque handle representing an event-type interest registration. */ registerInterest: async (params: RegisterEventInterestParams): Promise => - connection.sendRequest("session.eventLog.registerInterest", { sessionId, ...params }), + connection.sendRequest("session.eventLog.registerInterest", { ...params, sessionId }), /** * Releases a consumer's previously-registered interest in an event type. * @@ -32427,7 +34210,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the operation succeeded. */ releaseInterest: async (params: ReleaseEventInterestParams): Promise => - connection.sendRequest("session.eventLog.releaseInterest", { sessionId, ...params }), + connection.sendRequest("session.eventLog.releaseInterest", { ...params, sessionId }), }, /** @experimental */ usage: { @@ -32449,7 +34232,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Prediction result. Available results include prediction details; unavailable results include an explicit reason. */ predict: async (params?: SessionLimitPredictionPredictRequest): Promise => - connection.sendRequest("session.limitPrediction.predict", { sessionId, ...params }), + connection.sendRequest("session.limitPrediction.predict", { ...params, sessionId }), }, /** @experimental */ remote: { @@ -32461,7 +34244,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns GitHub URL for the session and a flag indicating whether remote steering is enabled. */ enable: async (params: RemoteEnableRequest): Promise => - connection.sendRequest("session.remote.enable", { sessionId, ...params }), + connection.sendRequest("session.remote.enable", { ...params, sessionId }), /** * Disables remote session export and steering. */ @@ -32475,7 +34258,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns 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. */ notifySteerableChanged: async (params: RemoteNotifySteerableChangedRequest): Promise => - connection.sendRequest("session.remote.notifySteerableChanged", { sessionId, ...params }), + connection.sendRequest("session.remote.notifySteerableChanged", { ...params, sessionId }), }, /** @experimental */ visibility: { @@ -32494,7 +34277,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Effective sharing status and shareable GitHub URL after updating session visibility. */ set: async (params: VisibilitySetRequest): Promise => - connection.sendRequest("session.visibility.set", { sessionId, ...params }), + connection.sendRequest("session.visibility.set", { ...params, sessionId }), }, /** @experimental */ schedule: { @@ -32513,7 +34296,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Remove a scheduled prompt by id. The result entry is omitted if the id was unknown. */ stop: async (params: ScheduleStopRequest): Promise => - connection.sendRequest("session.schedule.stop", { sessionId, ...params }), + connection.sendRequest("session.schedule.stop", { ...params, sessionId }), }, }; } @@ -32533,7 +34316,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @experimental */ sendSystemNotification: async (params: SendSystemNotificationRequest): Promise => - connection.sendRequest("session.sendSystemNotification", { sessionId, ...params }), + connection.sendRequest("session.sendSystemNotification", { ...params, sessionId }), /** @experimental */ gitHubAuth: { /** @@ -32541,7 +34324,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * * @returns Current authentication information, or null when no authentication is active. */ - getCurrentAuthInfo: async (): Promise => + getCurrentAuthInfo: async (): Promise => connection.sendRequest("session.gitHubAuth.getCurrentAuthInfo", { sessionId }), /** * Gets all authentication accounts available to the internal session host. @@ -32555,7 +34338,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * * @returns Current authentication information, or null when no authentication is active. */ - refreshCopilotUser: async (): Promise => + refreshCopilotUser: async (): Promise => connection.sendRequest("session.gitHubAuth.refreshCopilotUser", { sessionId }), /** * Logs in a GitHub user through the internal session host. @@ -32565,14 +34348,14 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Authentication credentials accepted only at native protocol ingress. Runtime outputs use credential-free `AuthIdentity` metadata. */ login: async (params: SessionAuthLoginRequest): Promise => - connection.sendRequest("session.gitHubAuth.login", { sessionId, ...params }), + connection.sendRequest("session.gitHubAuth.login", { ...params, sessionId }), /** * Switches the session to another available authentication. * * @param params Parameters for switching the session's active authentication. */ switchToAuth: async (params: SessionAuthSwitchRequest): Promise => - connection.sendRequest("session.gitHubAuth.switchToAuth", { sessionId, ...params }), + connection.sendRequest("session.gitHubAuth.switchToAuth", { ...params, sessionId }), /** * Logs out the session's current GitHub authentication. * @@ -32588,7 +34371,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Whether the requested authentication was logged out. */ logoutUser: async (params: SessionAuthLogoutUserRequest): Promise => - connection.sendRequest("session.gitHubAuth.logoutUser", { sessionId, ...params }), + connection.sendRequest("session.gitHubAuth.logoutUser", { ...params, sessionId }), /** * Gets validation errors from the most recent authentication attempt. * @@ -32607,14 +34390,14 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @param params Internal canvas provider registration parameters. */ register: async (params: CanvasProviderRegisterRequest): Promise => - connection.sendRequest("session.canvas.provider.register", { sessionId, ...params }), + connection.sendRequest("session.canvas.provider.register", { ...params, sessionId }), /** * Unregisters an internal canvas provider connection. * * @param params Internal canvas provider unregistration parameters. */ unregister: async (params: CanvasProviderUnregisterRequest): Promise => - connection.sendRequest("session.canvas.provider.unregister", { sessionId, ...params }), + connection.sendRequest("session.canvas.provider.unregister", { ...params, sessionId }), }, }, /** @experimental */ @@ -32627,7 +34410,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Complete current or terminal workflow run envelope. */ runFromTool: async (params: WorkflowToolRunRequest): Promise => - connection.sendRequest("session.workflow.runFromTool", { sessionId, ...params }), + connection.sendRequest("session.workflow.runFromTool", { ...params, sessionId }), /** * Internal tool-originated dynamic workflow resume. * @@ -32636,14 +34419,14 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Resolved persisted workflow identity and resumed run envelope. */ resumeFromTool: async (params: WorkflowToolResumeRequest): Promise => - connection.sendRequest("session.workflow.resumeFromTool", { sessionId, ...params }), + connection.sendRequest("session.workflow.resumeFromTool", { ...params, sessionId }), /** * Atomically pauses an owned dynamic workflow attempt at a durable checkpoint. * * @param params Parameters for an owned durable pause checkpoint. */ pauseAtCheckpoint: async (params: WorkflowPauseCheckpointRequest): Promise => - connection.sendRequest("session.workflow.pauseAtCheckpoint", { sessionId, ...params }), + connection.sendRequest("session.workflow.pauseAtCheckpoint", { ...params, sessionId }), }, /** @experimental */ model: { @@ -32655,7 +34438,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns The model identifier active on the session after the switch. */ applyStartupOverlay: async (params: ModelApplyStartupOverlayRequest): Promise => - connection.sendRequest("session.model.applyStartupOverlay", { sessionId, ...params }), + connection.sendRequest("session.model.applyStartupOverlay", { ...params, sessionId }), }, /** @experimental */ mcp: { @@ -32667,7 +34450,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns MCP server startup filtering result. */ reloadWithConfig: async (params: McpReloadWithConfigRequest): Promise => - connection.sendRequest("session.mcp.reloadWithConfig", { sessionId, ...params }), + connection.sendRequest("session.mcp.reloadWithConfig", { ...params, sessionId }), /** * Configures the built-in GitHub MCP server for the session's current auth context. * @@ -32676,21 +34459,21 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Result of configuring GitHub MCP. */ configureGitHub: async (params: McpConfigureGitHubRequest): Promise => - connection.sendRequest("session.mcp.configureGitHub", { sessionId, ...params }), + connection.sendRequest("session.mcp.configureGitHub", { ...params, sessionId }), /** * Registers a pre-connected external MCP client (e.g. IDE) on the session's host. The caller retains lifecycle ownership of the client and transport. Marked internal because the `client` and `transport` arguments are in-process MCP SDK instances that cannot be serialized across the JSON-RPC boundary; once the CLI moves on top of the SDK, external clients will be expressed as transport configs the runtime can construct itself. * * @param params Registration parameters for an external MCP client. */ registerExternalClient: async (params: McpRegisterExternalClientRequest): Promise => - connection.sendRequest("session.mcp.registerExternalClient", { sessionId, ...params }), + connection.sendRequest("session.mcp.registerExternalClient", { ...params, sessionId }), /** * 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. Disappears alongside `registerExternalClient`: once external clients are described to the runtime as config rather than handed in as instances, lifecycle (including deregistration) is owned entirely by the runtime. * * @param params Server name identifying the external client to remove. */ unregisterExternalClient: async (params: McpUnregisterExternalClientRequest): Promise => - connection.sendRequest("session.mcp.unregisterExternalClient", { sessionId, ...params }), + connection.sendRequest("session.mcp.unregisterExternalClient", { ...params, sessionId }), }, /** @experimental */ connectors: { @@ -32702,7 +34485,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included. */ reconcileForStartup: async (params: ConnectorAccountRequest): Promise => - connection.sendRequest("session.connectors.reconcileForStartup", { sessionId, ...params }), + connection.sendRequest("session.connectors.reconcileForStartup", { ...params, sessionId }), /** * Removes the runtime-owned Connector MCP projection without changing service-side connections. * @@ -32721,7 +34504,37 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Whether finalizing the invocation effect succeeded, and the failure reason when it did not. */ finalizeInvocationEffect: async (params: CommandsFinalizeInvocationEffectRequest): Promise => - connection.sendRequest("session.commands.finalizeInvocationEffect", { sessionId, ...params }), + connection.sendRequest("session.commands.finalizeInvocationEffect", { ...params, sessionId }), + }, + /** @experimental */ + ui: { + /** + * 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. + * + * @param 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. + */ + handleHumanAskUser: async (params: UIHandlePendingElicitationRequest): Promise => + connection.sendRequest("session.ui.handleHumanAskUser", { ...params, sessionId }), + /** + * Resolves a pending `user_input.requested` event after direct interaction in the trusted in-process client. + * + * @param 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. + */ + handleHumanUserInput: async (params: UIHandlePendingUserInputRequest): Promise => + connection.sendRequest("session.ui.handleHumanUserInput", { ...params, sessionId }), + /** + * Resolves a pending `exit_plan_mode.requested` event after direct interaction in the trusted in-process client. + * + * @param 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. + */ + handleHumanExitPlanMode: async (params: UIHandlePendingExitPlanModeRequest): Promise => + connection.sendRequest("session.ui.handleHumanExitPlanMode", { ...params, sessionId }), }, /** @experimental */ settings: { @@ -32740,7 +34553,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Result of evaluating a Rust-owned settings predicate. */ evaluatePredicate: async (params: SessionSettingsEvaluatePredicateRequest): Promise => - connection.sendRequest("session.settings.evaluatePredicate", { sessionId, ...params }), + connection.sendRequest("session.settings.evaluatePredicate", { ...params, sessionId }), }, /** @experimental */ queue: { @@ -32766,7 +34579,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Whether a deferred-idle drain should run. */ beginDeferredIdleDrain: async (params: QueueBeginDeferredIdleDrainRequest): Promise => - connection.sendRequest("session.queue.beginDeferredIdleDrain", { sessionId, ...params }), + connection.sendRequest("session.queue.beginDeferredIdleDrain", { ...params, sessionId }), /** * Finishes a native deferred-idle drain and reports whether to drain queue work or emit idle. * @@ -32775,14 +34588,14 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Action selected by the native deferred-idle drain. */ finishDeferredIdleDrain: async (params: QueueFinishDeferredIdleDrainRequest): Promise => - connection.sendRequest("session.queue.finishDeferredIdleDrain", { sessionId, ...params }), + connection.sendRequest("session.queue.finishDeferredIdleDrain", { ...params, sessionId }), /** * Marks session.idle as deferred by native background work state. * * @param params Inputs for marking session.idle deferred in native state. */ deferSessionIdle: async (params: QueueDeferSessionIdleRequest): Promise => - connection.sendRequest("session.queue.deferSessionIdle", { sessionId, ...params }), + connection.sendRequest("session.queue.deferSessionIdle", { ...params, sessionId }), /** * Consumes queued native system notifications matching an internal filter. * @@ -32791,7 +34604,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Indicates whether a user-facing pending item was removed. */ consumeSystemNotifications: async (params: QueueConsumeSystemNotificationsRequest): Promise => - connection.sendRequest("session.queue.consumeSystemNotifications", { sessionId, ...params }), + connection.sendRequest("session.queue.consumeSystemNotifications", { ...params, sessionId }), /** * Enqueues the internal resume-pending wake item when orphan handling needs a follow-up turn. * @@ -32827,7 +34640,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Result of registering or re-arming a scheduled prompt. */ add: async (params: ScheduleAddRequest): Promise => - connection.sendRequest("session.schedule.add", { sessionId, ...params }), + connection.sendRequest("session.schedule.add", { ...params, sessionId }), /** * Registers a recurring cron scheduled prompt. * @@ -32836,7 +34649,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Result of registering or re-arming a scheduled prompt. */ addCron: async (params: ScheduleAddCronRequest): Promise => - connection.sendRequest("session.schedule.addCron", { sessionId, ...params }), + connection.sendRequest("session.schedule.addCron", { ...params, sessionId }), /** * Registers an absolute-time scheduled prompt. * @@ -32845,7 +34658,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Result of registering or re-arming a scheduled prompt. */ addAt: async (params: ScheduleAddAtRequest): Promise => - connection.sendRequest("session.schedule.addAt", { sessionId, ...params }), + connection.sendRequest("session.schedule.addAt", { ...params, sessionId }), /** * Registers a self-paced scheduled prompt. * @@ -32854,7 +34667,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Result of registering or re-arming a scheduled prompt. */ addSelfPaced: async (params: ScheduleAddSelfPacedRequest): Promise => - connection.sendRequest("session.schedule.addSelfPaced", { sessionId, ...params }), + connection.sendRequest("session.schedule.addSelfPaced", { ...params, sessionId }), /** * Re-arms an active self-paced scheduled prompt. * @@ -32863,7 +34676,7 @@ export function createInternalSessionRpc(connection: MessageConnection, sessionI * @returns Result of registering or re-arming a scheduled prompt. */ rearmSelfPaced: async (params: ScheduleRearmSelfPacedRequest): Promise => - connection.sendRequest("session.schedule.rearmSelfPaced", { sessionId, ...params }), + connection.sendRequest("session.schedule.rearmSelfPaced", { ...params, sessionId }), }, }; } @@ -32926,6 +34739,14 @@ export interface SessionFsHandler { * @returns File content as a UTF-8 string, or a filesystem error if the read failed. */ readFile(params: SessionFsReadFileRequest): Promise; + /** + * Reads binary file content from the client-provided session filesystem. + * + * @param params 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(params: SessionFsReadFileBytesRequest): Promise; /** * Writes a file in the client-provided session filesystem. * @@ -32934,6 +34755,14 @@ export interface SessionFsHandler { * @returns Describes a filesystem error. */ writeFile(params: SessionFsWriteFileRequest): Promise; + /** + * Writes binary file content to the client-provided session filesystem. + * + * @param params File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem. + * + * @returns Describes a filesystem error. + */ + writeFileBytes(params: SessionFsWriteFileBytesRequest): Promise; /** * Appends content to a file in the client-provided session filesystem, creating parent directories as needed. * @@ -33095,11 +34924,21 @@ export function registerClientSessionApiHandlers( if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`); return handler.readFile(params); }); + connection.onRequest("sessionFs.readFileBytes", async (params: SessionFsReadFileBytesRequest) => { + const handler = getHandlers(params.sessionId).sessionFs; + if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`); + return handler.readFileBytes(params); + }); connection.onRequest("sessionFs.writeFile", async (params: SessionFsWriteFileRequest) => { const handler = getHandlers(params.sessionId).sessionFs; if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`); return handler.writeFile(params); }); + connection.onRequest("sessionFs.writeFileBytes", async (params: SessionFsWriteFileBytesRequest) => { + const handler = getHandlers(params.sessionId).sessionFs; + if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`); + return handler.writeFileBytes(params); + }); connection.onRequest("sessionFs.appendFile", async (params: SessionFsAppendFileRequest) => { const handler = getHandlers(params.sessionId).sessionFs; if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`); diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 96f6f247c5..9146818e4c 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -76,6 +76,7 @@ export type SessionEvent = | ToolUserRequestedEvent | ToolExecutionStartEvent | ToolExecutionPartialResultEvent + | ToolShellOutputEvent | ToolExecutionProgressEvent | ToolExecutionCompleteEvent | ToolSearchActivatedEvent @@ -128,6 +129,7 @@ export type SessionEvent = | CapabilitiesChangedEvent | ExitPlanModeRequestedEvent | ExitPlanModeCompletedEvent + | HumanResponseRecordedEvent | ToolsUpdatedEvent | BackgroundTasksChangedEvent | WorkflowRunUpdatedEvent @@ -945,6 +947,28 @@ export type ToolExecutionStartToolDescriptionMetaUIVisibility = | "model" /** Tool is callable by the MCP App view (iframe) via session.mcp.apps.callTool */ | "app"; +/** + * Shell output source. Terminal output has no separate stdout/stderr attribution. + */ +export type ToolShellOutputStream = + /** Output from the shell command's standard output stream. This is the default when stream is omitted. */ + | "stdout" + /** Output from the shell command's standard error stream. */ + | "stderr" + /** Inherently merged output that cannot be attributed separately to stdout or stderr. */ + | "terminal"; +/** + * Kind of file mutation committed by a built-in editing tool. + */ +/** @experimental */ +export type ToolExecutionCompleteFileEditKind = + /** A file was created. */ + | "create" + /** A file was written by an edit operation. */ + | "edit" + /** A file was deleted. */ + | "delete" + | (string & {}); /** * A model-facing binary result as persisted: full inline data, a size-omitted marker, or a deduplicated asset reference */ @@ -1449,6 +1473,95 @@ export type ExitPlanModeAction = | "autopilot" /** Exit plan mode and continue with parallel autonomous workers. */ | "autopilot_fleet"; +/** + * Controlled provenance for a typed runtime response. Only `human_response`, minted by a trusted direct-interaction ingress, is human authorization evidence. + */ +export type HumanResponseActor = + /** A built-in trusted client submitted the response after direct human interaction. */ + | "human_response" + /** A host or SDK automation submitted the response without direct human interaction. */ + | "host_automation" + /** The response came through a legacy or otherwise unattributed ingress. */ + | "unknown"; +/** + * Exact runtime-owned question or reviewed plan paired with the typed response that settled it. + */ +export type HumanResponseRecordedResponse = + | { + /** + * Exact answer content accepted from the user. + */ + content: { + [k: string]: ElicitationCompletedContent | undefined; + }; + /** + * Exact question displayed to the user. + */ + message: string; + requestedSchema: ElicitationRequestedSchema; + /** + * Runtime-owned response kind discriminator. + */ + responseKind: "ask_user"; + } + | { + /** + * Whether the displayed request allowed a free-form answer. + */ + allowFreeform?: boolean; + /** + * Exact selected or free-form answer submitted by the user. + */ + answer: string; + /** + * Exact choices displayed to the user, when the request offered choices. + */ + choices?: string[]; + /** + * Exact question displayed to the user. + */ + question: string; + /** + * Runtime-owned response kind discriminator. + */ + responseKind: "user_input"; + /** + * Whether the answer was typed as free-form text rather than selected from the displayed choices. + */ + wasFreeform: boolean; + } + | { + /** + * Actions offered by the plan review UI. + */ + actions: ExitPlanModeAction[]; + /** + * Whether the user approved the reviewed plan. + */ + approved: boolean; + /** + * Whether the selected response requested edit auto-approval. + */ + autoApproveEdits?: boolean; + /** + * Exact feedback submitted with the plan decision, when present. + */ + feedback?: string; + /** + * Exact full plan content available from the review UI. + */ + planContent: string; + recommendedAction: ExitPlanModeAction; + /** + * Runtime-owned response kind discriminator. + */ + responseKind: "exit_plan_mode"; + selectedAction?: ExitPlanModeAction; + /** + * Exact plan summary displayed to the user. + */ + summary: string; + }; /** * 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. */ @@ -1505,7 +1618,7 @@ export type McpServerStatus = | "pending" /** The server is configured but 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. */ | "stopped" /** The server is not configured for this session. */ | "not_configured"; @@ -1595,6 +1708,10 @@ export interface StartData { * Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") */ reasoningEffort?: string; + /** + * Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + */ + reasoningEffortModel?: string; reasoningSummary?: ReasoningSummary; /** * Whether this session supports remote steering via GitHub @@ -1747,6 +1864,10 @@ export interface ResumeData { * Reasoning effort level used for model calls, if applicable (e.g. "none", "low", "medium", "high", "xhigh", "max") */ reasoningEffort?: string; + /** + * Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + */ + reasoningEffortModel?: string; reasoningSummary?: ReasoningSummary; /** * Whether this session supports remote steering via GitHub @@ -2349,6 +2470,10 @@ export interface ModelChangeData { * Reasoning effort level after the model change, if applicable */ reasoningEffort?: string | null; + /** + * Model that owns effort embedded in an authored model selection. Omitted for independent reasoning-effort overrides and legacy events. + */ + reasoningEffortModel?: string; reasoningSummary?: ReasoningSummary; source?: ModelChangeSource; verbosity?: Verbosity; @@ -5853,6 +5978,10 @@ export interface 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. */ blocks?: JsonValue[]; + /** + * 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?: JsonValue[]; /** * Model provider that produced these reasoning blocks. */ @@ -6157,6 +6286,10 @@ export interface AssistantUsageData { * @internal */ availableToolCount?: number; + /** + * 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; /** * Whether the provider reported prompt-cache usage details for this call * @@ -6248,6 +6381,10 @@ export interface AssistantUsageData { * Model identifier used for this API call */ 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. + */ + modelProvider?: string; /** * Number of tool calls returned by the model * @@ -6488,6 +6625,10 @@ export interface ModelCallFailureData { apiCallId?: string; apiEndpoint?: AssistantUsageApiEndpoint; badRequestKind?: ModelCallFailureBadRequestKind; + /** + * 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; /** * Duration of the failed API call in milliseconds */ @@ -6539,6 +6680,10 @@ export interface ModelCallFailureData { * Model identifier used for the failed API call */ model?: string; + /** + * 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; /** * Parent task tool call ID when this failed model call belongs to a sub-agent */ @@ -6894,7 +7039,8 @@ export interface ToolExecutionStartToolDescriptionMetaUI { visibility?: ToolExecutionStartToolDescriptionMetaUIVisibility[]; } /** - * Session event "tool.execution_partial_result". Streaming tool execution output for incremental result display + * @deprecated + * Session event "tool.execution_partial_result". Deprecated merged replacement snapshot of shell output. Use tool.shell_output for append-only, stream-tagged output instead. */ export interface ToolExecutionPartialResultEvent { /** @@ -6924,11 +7070,12 @@ export interface ToolExecutionPartialResultEvent { type: "tool.execution_partial_result"; } /** - * Streaming tool execution output for incremental result display + * @deprecated + * Deprecated merged replacement snapshot of shell output. Use tool.shell_output for append-only, stream-tagged output instead. */ export interface ToolExecutionPartialData { /** - * Incremental output chunk from the running tool + * Merged replacement snapshot from the running shell, not an append-only chunk */ partialOutput: string; /** @@ -6936,6 +7083,54 @@ export interface ToolExecutionPartialData { */ toolCallId: 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. + */ +export interface ToolShellOutputEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: ToolShellOutputData; + /** + * Always true for events that are transient and not persisted to the session event log on disk. + */ + ephemeral: true; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "tool.shell_output". + */ + type: "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. + */ +export interface ToolShellOutputData { + /** + * Zero-based publication sequence across all output streams for this tool call. Not a byte offset or an OS write-order guarantee. + */ + sequence: number; + stream?: ToolShellOutputStream; + /** + * New output to append, without synthetic shell-result markers or stream-switch separators + */ + text: string; + /** + * Tool call ID that owns this shell output + */ + toolCallId: string; +} /** * Session event "tool.execution_progress". Tool execution progress notification with status message */ @@ -6974,6 +7169,12 @@ export interface ToolExecutionProgressData { * Human-readable progress status message (e.g., from an MCP server) */ progressMessage: string; + /** + * Client-only structured progress metadata. Not model-facing tool output. + * + * @experimental + */ + structuredContent?: JsonValue; /** * Tool call ID this progress notification belongs to */ @@ -7014,6 +7215,12 @@ export interface ToolExecutionCompleteEvent { */ export interface ToolExecutionCompleteData { error?: ToolExecutionCompleteError; + /** + * 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?: ToolExecutionCompleteFileEdit[]; /** * Experimental HydraFusion attribution for this tool completion. * @@ -7092,6 +7299,17 @@ export interface ToolExecutionCompleteError { message: string; remediation?: RemediationAction; } +/** + * A file mutation that was actually committed by a built-in file editing tool. + */ +/** @experimental */ +export interface ToolExecutionCompleteFileEdit { + kind: ToolExecutionCompleteFileEditKind; + /** + * Absolute path in the session filesystem namespace. + */ + path: string; +} /** * Tool execution result on success */ @@ -11801,13 +12019,17 @@ export interface AutoModeResolvedData { * The routing method the server applied, when Auto Intent ran */ routingMethod?: string; + /** + * 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; /** * Whether a sticky model choice overrode the router result */ stickyOverride?: boolean; } /** - * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. + * Session event "session.managed_settings_resolved". Effective enterprise managed settings applied to the session and their contributing channels. Emitted whenever managed policy is applied or reapplied, including session start, resume, and account switch. This ephemeral live snapshot is delivered to subscribers but not persisted to the session event log; initial resolution occurs before session.start. */ /** @experimental */ export interface ManagedSettingsResolvedEvent { @@ -11838,7 +12060,7 @@ export interface ManagedSettingsResolvedEvent { type: "session.managed_settings_resolved"; } /** - * Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. + * Effective enterprise managed settings and contributing channels. Session events report applied policy; sessionless resolve reports an account/device snapshot, and compose reports a non-applying preview of candidate documents. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively. Session-local SDK-client policy is included only in session results. Marked experimental while the managed-settings surface stabilizes. */ /** @experimental */ export interface ManagedSettingsResolvedData { @@ -12148,6 +12370,51 @@ export interface ExitPlanModeCompletedData { requestId: string; selectedAction?: ExitPlanModeAction; } +/** + * Session event "human_response.recorded". Durable request-correlated evidence for a typed response to a runtime-owned question or plan review. + */ +export interface HumanResponseRecordedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: HumanResponseRecordedData; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "human_response.recorded". + */ + type: "human_response.recorded"; +} +/** + * Durable request-correlated evidence for a typed response to a runtime-owned question or plan review. + */ +export interface HumanResponseRecordedData { + actor: HumanResponseActor; + /** + * Request ID of the runtime-owned question or plan review. + */ + requestId: string; + response: HumanResponseRecordedResponse; + /** + * Tool call ID that opened the request, when present. + */ + toolCallId?: string; +} /** * Session event "session.tools_updated". Payload of `session.tools_updated` identifying the model whose resolved tools were updated. */ @@ -12651,7 +12918,7 @@ export interface McpServerStatusChangedEvent { */ export interface McpServerStatusChangedData { /** - * 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; /** diff --git a/nodejs/src/host.ts b/nodejs/src/host.ts index 7ae4036531..28d64b89f6 100644 --- a/nodejs/src/host.ts +++ b/nodejs/src/host.ts @@ -6,6 +6,8 @@ import type { HostExitedNotification, HostStartResult, HostPublishSessionResult, + HostLocalServerOptions, + HostGitHubEnvironmentOptions, } from "./generated/rpc.js"; import type { CopilotSession } from "./session.js"; import type { SessionConfig, ResumeSessionConfig } from "./types.js"; @@ -36,19 +38,16 @@ export interface AhpSessionResumeRequest { export type AhpHostExit = HostExitedNotification; /** - * Options for a runtime-supervised AHP listener. Callbacks are captured at startup; + * Options for runtime-supervised AHP hosting. Select at least one transport. + * Callbacks are captured at startup; * later changes to this options object do not reconfigure an existing host. * @experimental */ export interface AhpHostOptions { - /** Listener hostname. The runtime defaults to 127.0.0.1. */ - hostname?: string; - /** Listener port. Omitted or zero asks the runtime for an available port. */ - port?: number; - /** Connection token. The runtime generates one when required and omitted. */ - token?: string; - /** Whether the listener requires token authentication. */ - requireConnectionToken?: boolean; + /** Enable a local WebSocket listener; an empty object selects loopback defaults. */ + localServer?: HostLocalServerOptions; + /** Register a Mission Control environment and enable remote WPS connections. */ + githubEnvironment?: HostGitHubEnvironmentOptions; /** Called at most once when hosting ends, not on runtime process death. Local, never serialized. */ onExit?: (exit: AhpHostExit) => void; /** @@ -84,7 +83,10 @@ export interface AhpHostOptions { */ export class AhpHost { readonly hostId: string; - readonly url: string; + /** Local WebSocket URL. Absent when only the GitHub environment is enabled. */ + readonly url: string | undefined; + /** Mission Control environment ID, when GitHub hosting is enabled. */ + readonly environmentId: string | undefined; /** Connection token, when required by the listener. Treat this value as a secret. */ readonly token: string | undefined; /** Legacy separate host process ID. Absent for in-process listeners; use dispose() to stop. */ @@ -100,6 +102,7 @@ export class AhpHost { ) { this.hostId = info.hostId; this.url = info.url; + this.environmentId = info.environmentId; this.token = info.token; this.pid = info.pid; } diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 5ce938d368..897be6cb1c 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -9,6 +9,7 @@ */ export { CopilotClient } from "./client.js"; +export type { HostLocalServerOptions, HostGitHubEnvironmentOptions } from "./generated/rpc.js"; export { AhpHost, type AhpHostExit, @@ -42,6 +43,7 @@ export { CopilotWebSocketCloseStatus, CopilotWebSocketForwarder, SessionFsSqliteTransactionFailure, + SessionFsWriteFailure, SYSTEM_MESSAGE_SECTIONS, } from "./types.js"; // Re-export the generated session-event types (every *Event interface and @@ -67,6 +69,8 @@ export type { CanvasProviderIdentity, CloudSessionOptions, CloudSessionRepository, + AuthIdentityMetadata, + AuthInfoType, ConnectorAccountRequest, ConnectorAvailability, ConnectorCapabilities, @@ -80,6 +84,8 @@ export type { ConnectorMcpStatus, ConnectorReconcileRequest, ConnectorRuntimeStatus, + ConnectorSessionAccount, + ConnectorSessionAccountResult, ConnectorStatus, AutoModeSwitchHandler, AutoModeSwitchRequest, @@ -87,6 +93,12 @@ export type { AgentStopHandler, AgentStopHookInput, AgentStopHookOutput, + SubagentStartHandler, + SubagentStartHookInput, + SubagentStartHookOutput, + SubagentStopHandler, + SubagentStopHookInput, + SubagentStopHookOutput, UserPromptTransformedHandler, UserPromptTransformedHookInput, UserPromptTransformedHookOutput, @@ -175,11 +187,13 @@ export type { PermissionDecisionSurface, PermissionResponseCapability, ProviderConfig, + ProviderConfigModelProvider, ProviderModelConfig, ProviderTokenArgs, RemoteSessionMode, SandboxConfigSource, ResumeSessionConfig, + TranscriptRecovery, SectionOverride, SectionOverrideAction, SectionTransformFn, diff --git a/nodejs/src/runtimeArtifacts.ts b/nodejs/src/runtimeArtifacts.ts index d33b72c9da..9f749486ac 100644 --- a/nodejs/src/runtimeArtifacts.ts +++ b/nodejs/src/runtimeArtifacts.ts @@ -43,6 +43,7 @@ const EXCLUDED_TOP_LEVEL = new Set([ "app.js", "assets", "changelog.json", + "cli-main.js", "copilot", "copilot.exe", "foundry-local-sdk", diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 39e023a7bc..5790ff2643 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -16,6 +16,7 @@ import type { CanvasActionInvokeResult, CurrentToolMetadata, McpOauthPendingRequestResponse, + ProtocolExternalToolDefinition, WorkflowLogLine, WorkflowRunResult as WireWorkflowRunResult, ModelSwitchAutoTierResult, @@ -64,6 +65,7 @@ import type { ToolResult, ToolResultObject, TraceContextProvider, + TranscriptRecovery, TypedSessionEventHandler, UserInputHandler, UserInputRequest, @@ -111,6 +113,23 @@ function copyDefinedWorkflowAgentOption } } +/** + * The wire definition `session.tools.set` takes for a tool, matching what + * session creation and resumption send for the same tool. + */ +function toToolDefinition(tool: Tool): ProtocolExternalToolDefinition { + return { + name: tool.name, + description: tool.description ?? "", + parameters: toJsonSchema(tool.parameters) as ProtocolExternalToolDefinition["parameters"], + overridesBuiltInTool: tool.overridesBuiltInTool, + skipPermission: tool.skipPermission, + defer: tool.defer, + metadata: tool.metadata as ProtocolExternalToolDefinition["metadata"], + isTerminal: tool.isTerminal, + }; +} + type WorkflowExecutionContext = { active: boolean; helperScope?: "parallel" | "pipeline"; @@ -425,10 +444,13 @@ const TOOL_SEARCH_TOOL_NAME = "tool_search_tool"; */ export class CopilotSession { + private _transcriptRecovery?: TranscriptRecovery; private eventHandlers: Set = new Set(); private typedEventHandlers: Map void>> = new Map(); private toolHandlers: Map = new Map(); + /** Settles once every earlier `setTools` call has finished. */ + private setToolsQueue: Promise = Promise.resolve(); private pendingExternalTools: Map = new Map(); private canvases: Map = new Map(); private bearerTokenProviders: Map = new Map(); @@ -670,6 +692,16 @@ export class CopilotSession { return this._workspacePath; } + /** Recovery observed while loading this session; undefined on a clean resume. */ + get transcriptRecovery(): TranscriptRecovery | undefined { + return this._transcriptRecovery; + } + + /** @internal */ + setTranscriptRecovery(recovery: TranscriptRecovery | undefined): void { + this._transcriptRecovery = recovery; + } + /** * Host capabilities reported when the session was created or resumed. * Use this to check feature support before calling capability-gated APIs. @@ -1522,7 +1554,29 @@ export class CopilotSession { for (const tool of tools) { if (tool.handler) { - this.toolHandlers.set(tool.name, tool.handler); + const handler = tool.handler; + if ( + tool.name === "apply_patch" && + tool.overridesBuiltInTool && + toJsonSchema(tool.parameters)?.type === "string" + ) { + this.toolHandlers.set(tool.name, (args, invocation) => { + if (typeof args === "string") { + return handler(args, invocation); + } + if ( + typeof args === "object" && + args !== null && + "input" in args && + typeof args.input === "string" + ) { + return handler(args.input, invocation); + } + throw new TypeError("apply_patch string override requires a string input"); + }); + } else { + this.toolHandlers.set(tool.name, handler); + } } } } @@ -2196,6 +2250,8 @@ export class CopilotSession { sessionEnd: this.hooks.onSessionEnd as GenericHandler | undefined, errorOccurred: this.hooks.onErrorOccurred as GenericHandler | undefined, agentStop: this.hooks.onAgentStop as GenericHandler | undefined, + subagentStart: this.hooks.onSubagentStart as GenericHandler | undefined, + subagentStop: this.hooks.onSubagentStop as GenericHandler | undefined, }; const handler = handlerMap[hookType]; @@ -2390,6 +2446,60 @@ export class CopilotSession { return await this.rpc.model.switchAutoTier({ autoTier }); } + /** + * Replace the tools this client supplies to the session. + * + * `tools` becomes the complete set of tools this client implements, + * replacing the ones it supplied when the session was created or resumed, + * or in an earlier call. Built-in, MCP, and plugin tools, and tools other + * connected clients supply, are unaffected. Pass an empty array to remove + * all of this client's tools. + * + * Tools are defined the same way as for `createSession`: calls to tools + * with a `handler` are dispatched to it, and tools without one are + * declaration-only. Once the runtime accepts the replacement, every tool + * call this session dispatches uses the new handlers; calls already + * running finish on their original handlers. If the runtime rejects the + * replacement, this rejects and the previous tools and handlers stay in + * place. Concurrent calls on the same session are applied one at a time, + * in the order they are made. + * + * 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. + * + * @param tools - The complete set of tools this client supplies + * + * @experimental Wraps the experimental `session.tools.set` RPC and may change + * or be removed in a future release. + * + * @example + * ```typescript + * await session.setTools([ + * defineTool("search_issues", { + * description: "Search the issues shown on the current page", + * parameters: z.object({ query: z.string() }), + * handler: async ({ query }) => searchIssues(query), + * }), + * ]); + * ``` + */ + async setTools(tools: Tool[]): Promise { + const definitions = tools.map(toToolDefinition); + const replacement = this.setToolsQueue.then(async () => { + await this.rpc.tools.set({ tools: definitions }); + this.registerTools(tools); + }); + this.setToolsQueue = replacement.then( + () => undefined, + () => undefined + ); + await replacement; + } + /** * Log a message to the session timeline. * The message appears in the session event stream and is visible to SDK consumers diff --git a/nodejs/src/sessionFsProvider.ts b/nodejs/src/sessionFsProvider.ts index ecb18a5702..ee9908682d 100644 --- a/nodejs/src/sessionFsProvider.ts +++ b/nodejs/src/sessionFsProvider.ts @@ -13,6 +13,9 @@ import type { SessionFsSqliteQueryType, } from "./generated/rpc.js"; +const MAX_BINARY_BYTES = ((64 * 1024 * 1024 - 1024) / 4) * 3; +const MAX_BINARY_CONTENT_LENGTH = Math.ceil(MAX_BINARY_BYTES / 3) * 4; + export type { SessionFsSqliteQueryType, SessionFsSqliteTransactionErrorClass }; /** @@ -63,6 +66,11 @@ export class SessionFsSqliteTransactionFailure extends Error { } } +/** Throw from `writeFile` only when the provider changed the target before failing. */ +export class SessionFsWriteFailure extends Error { + readonly writeChanged = true; +} + /** * SQLite operations for the per-session database. * Implementers provide query execution and existence checking. @@ -112,7 +120,13 @@ export interface SessionFsProvider { /** Reads the full content of a file. Throw if the file does not exist. */ readFile(path: string): Promise; - /** Writes content to a file, creating parent directories if needed. */ + /** Read exact file bytes. Required when capabilities.binary is enabled. */ + readFileBytes?(path: string): Promise; + + /** Write exact file bytes. Required when capabilities.binary is enabled. */ + writeFileBytes?(path: string, content: Uint8Array, mode?: number): Promise; + + /** Writes content to a file, creating parent directories if needed. Throw {@link SessionFsWriteFailure} if a failed write changed the target. */ writeFile(path: string, content: string, mode?: number): Promise; /** Appends content to a file, creating parent directories if needed. */ @@ -174,10 +188,63 @@ export function createSessionFsAdapter(provider: SessionFsProvider): SessionFsHa return { content: "", error: toSessionFsError(err) }; } }, + readFileBytes: async ({ path }) => { + if (!provider.readFileBytes) { + return { + content: "", + error: { code: "UNKNOWN", message: "Binary reads are not supported" }, + }; + } + try { + const bytes = await provider.readFileBytes(path); + if (bytes.length > MAX_BINARY_BYTES) { + return { + content: "", + error: { + code: "UNKNOWN", + message: + "sessionFs.readFileBytes content exceeds the binary read limit", + }, + }; + } + return { + content: Buffer.from(bytes).toString("base64"), + }; + } catch (err) { + return { content: "", error: toSessionFsError(err) }; + } + }, writeFile: async ({ path, content, mode }) => { try { await provider.writeFile(path, content, mode); return undefined; + } catch (err) { + const error = toSessionFsError(err); + return err instanceof SessionFsWriteFailure + ? { ...error, writeChanged: true } + : error; + } + }, + writeFileBytes: async ({ path, content, mode }) => { + if (!provider.writeFileBytes) { + return { code: "UNKNOWN", message: "Binary writes are not supported" }; + } + if (content.length > MAX_BINARY_CONTENT_LENGTH) { + return { + code: "UNKNOWN", + message: "sessionFs.writeFileBytes content exceeds the binary write limit", + }; + } + const bytes = Buffer.from(content, "base64"); + if (bytes.toString("base64") !== content || bytes.length > MAX_BINARY_BYTES) { + return { + code: "UNKNOWN", + message: "invalid sessionFs.writeFileBytes base64 content", + }; + } + try { + await provider.writeFileBytes(path, bytes, mode); + return undefined; } catch (err) { return toSessionFsError(err); } diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index cc64c3516d..7b0f1fcf8e 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -40,6 +40,8 @@ export type { RemoteSessionMode } from "./generated/rpc.js"; export type { CurrentToolMetadata } from "./generated/rpc.js"; export type { SandboxConfigSource } from "./generated/rpc.js"; export type { + AuthIdentityMetadata, + AuthInfoType, ConnectorAccountRequest, ConnectorAvailability, ConnectorCapabilities, @@ -53,6 +55,8 @@ export type { ConnectorMcpStatus, ConnectorReconcileRequest, ConnectorRuntimeStatus, + ConnectorSessionAccount, + ConnectorSessionAccountResult, ConnectorStatus, ExtensionLaunchProfile, ExtensionLaunchProviderResolveRequest, @@ -116,6 +120,7 @@ export type { SessionFsSqliteProvider } from "./sessionFsProvider.js"; export type { SessionFsSqliteStatement } from "./sessionFsProvider.js"; export type { SessionFsSqliteTransactionErrorClass } from "./sessionFsProvider.js"; export { SessionFsSqliteTransactionFailure } from "./sessionFsProvider.js"; +export { SessionFsWriteFailure } from "./sessionFsProvider.js"; export type { LlmInferenceHeaders } from "./generated/rpc.js"; export type { PermissionDecisionContext, @@ -1152,11 +1157,11 @@ export const SYSTEM_MESSAGE_SECTIONS: Record Promise | AgentStopHookOutput | void; +/** + * Input for the hook fired before a sub-agent's first turn. + * + * The session metadata belongs to the parent session, not the child. + */ +export interface SubagentStartHookInput extends BaseHookInput { + transcriptPath: string; + agentName: string; + agentDisplayName?: string; + agentDescription?: string; +} + +/** Output for the sub-agent start hook. Context is prepended to the child's initial prompt. */ +export interface SubagentStartHookOutput { + additionalContext?: string; +} + +/** Handler for the sub-agent start hook. */ +export type SubagentStartHandler = ( + input: SubagentStartHookInput, + invocation: { sessionId: string } +) => Promise | SubagentStartHookOutput | void; + +/** + * Input for the hook fired after a sub-agent completes a turn. + * + * The response is the child's last assistant message before any hook rewrite. + */ +export interface SubagentStopHookInput extends SubagentStartHookInput { + agentId?: string; + agentType: string; + stopReason: "end_turn"; + response: string; +} + +/** + * Output for the sub-agent stop hook. `"block"` with a nonempty `reason` continues + * the child; otherwise `modifiedResponse` replaces the response reported to the parent. + * When both are supplied, a valid block takes precedence over the rewrite. + */ +export type SubagentStopHookOutput = + | { decision: "block"; reason: string; modifiedResponse?: string } + | { decision?: "allow"; reason?: never; modifiedResponse?: string }; + +/** Handler for the sub-agent stop hook. */ +export type SubagentStopHandler = ( + input: SubagentStopHookInput, + invocation: { sessionId: string } +) => Promise | SubagentStopHookOutput | void; + /** * Configuration for session hooks */ @@ -1851,6 +1909,15 @@ export interface SessionHooks { * agent stop. */ onAgentStop?: AgentStopHandler; + + /** Called before a sub-agent's first turn. Return context to prepend to its prompt. */ + onSubagentStart?: SubagentStartHandler; + + /** + * Called after a sub-agent completes a turn. Return a block reason to + * continue the child, or a replacement response to report to the parent. + */ + onSubagentStop?: SubagentStopHandler; } // ============================================================================ @@ -3015,6 +3082,11 @@ export interface SessionConfig extends SessionConfigBase { * {@link CopilotClient.resumeSession}. */ export interface ResumeSessionConfig extends SessionConfigBase { + /** + * Allow the runtime to recover a damaged transcript during resume. + * Defaults to true in all modes. Set false to reject recovery. + */ + allowTranscriptRecovery?: boolean; /** * When true, skips emitting the session.resume event. * Useful for reconnecting to a session without triggering resume-related side effects. @@ -3024,7 +3096,8 @@ export interface ResumeSessionConfig extends SessionConfigBase { /** * When true, the runtime continues any tool calls or permission prompts that were * still pending when the session was last suspended. When false (the default), the - * runtime treats pending work as interrupted on resume. + * runtime treats 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, @@ -3041,6 +3114,16 @@ export interface ResumeSessionConfig extends SessionConfigBase { openCanvases?: OpenCanvasInstance[]; } +/** Transcript repair proposed during the most recent resume. */ +export interface TranscriptRecovery { + /** Planned backup path; the backup is written on the next append. */ + plannedBackupPath: string; + /** One-based physical line numbers removed from the transcript. */ + invalidLineNumbers: number[]; + /** Whether an existing session.start event was moved to the beginning. */ + sessionStartMoved: boolean; +} + /** * Options that only an extension join may supply, kept off {@link ResumeSessionConfig} * because the runtime ignores them for every other kind of connection. @@ -3096,6 +3179,20 @@ export interface ProviderTokenArgs { */ export type BearerTokenProvider = (args: ProviderTokenArgs) => Promise; +/** + * Product serving a configured provider's model. Allowed values are + * "openai", "anthropic", "azure_openai", "ollama", "lm_studio", + * "foundry_local", and "llama_cpp". + */ +export type ProviderConfigModelProvider = + | "openai" + | "anthropic" + | "azure_openai" + | "ollama" + | "lm_studio" + | "foundry_local" + | "llama_cpp"; + /** * Configuration for a custom API provider. */ @@ -3121,6 +3218,12 @@ export interface ProviderConfig { */ transport?: "http" | "websockets"; + /** + * Product serving the model, such as "ollama" or "lm_studio", reported in + * telemetry as `model_provider`. Only affects telemetry. + */ + modelProvider?: ProviderConfigModelProvider; + /** * API endpoint URL */ @@ -3227,6 +3330,12 @@ export interface NamedProviderConfig { */ wireApi?: "completions" | "responses"; + /** + * Product serving this provider's models, such as "ollama" or "lm_studio", + * reported in telemetry as `model_provider`. Only affects telemetry. + */ + modelProvider?: ProviderConfigModelProvider; + /** * API endpoint URL. */ @@ -3500,6 +3609,12 @@ export interface SessionFsConfig { * @default false */ sqlite?: boolean; + /** + * Whether this provider supports exact binary reads and writes through readFileBytes and writeFileBytes. + * Required to view images stored only in the provider. + * @default false + */ + binary?: boolean; }; } diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 174f911287..d0466fb7a2 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -1,10 +1,18 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { EventEmitter } from "node:events"; +import { EventEmitter, once } from "node:events"; import { PassThrough } from "stream"; -import { mkdtempSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { createConnection, createServer, type Socket } from "node:net"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { describe, expect, it, onTestFinished, vi } from "vitest"; +import { + createMessageConnection, + StreamMessageReader, + StreamMessageWriter, + type MessageConnection, +} from "vscode-jsonrpc/node.js"; import { approveAll, createAttributedPermissionResult, @@ -17,9 +25,13 @@ import { type GitHubTelemetryNotification, type ManagedSettings, type ModelInfo, + type SubagentStartHookOutput, + type SubagentStopHookOutput, + type SessionFsProvider, } from "../src/index.js"; import { CopilotSession } from "../src/session.js"; import { defaultJoinSessionPermissionHandler } from "../src/types.js"; +import { getSdkProtocolVersion } from "../src/sdkProtocolVersion.js"; // This file is for unit tests. Where relevant, prefer to add e2e tests in e2e/*.test.ts instead @@ -193,7 +205,7 @@ describe("CopilotClient", () => { const stdin = new PassThrough(); const stdout = new PassThrough(); - (client as any).cliProcess = { stdin, stdout }; + (client as any).cliProcess = Object.assign(new EventEmitter(), { stdin, stdout }); await (client as any).connectToChildProcessViaStdio(); const dispose = vi.spyOn((client as any).connection, "dispose"); @@ -960,6 +972,48 @@ describe("CopilotClient", () => { expect(cliResumePayload.isExperimentalMode).toBeUndefined(); }); + it.each(["empty", "copilot-cli"] as const)( + "preserves transcript recovery overrides in %s mode and exposes the resume report", + async (mode) => { + const baseDirectory = mkdtempSync(join(tmpdir(), "copilot-sdk-node-recovery-")); + const client = new CopilotClient({ mode, baseDirectory }); + await client.start(); + onTestFinished(() => client.forceStop()); + + const recovery = { + plannedBackupPath: "events.jsonl.backup", + invalidLineNumbers: [2], + sessionStartMoved: false, + }; + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") { + return { sessionId: params.sessionId, transcriptRecovery: recovery }; + } + if (method === "session.options.update") return {}; + throw new Error(`Unexpected method: ${method}`); + }); + + const created = await client.createSession({ + onPermissionRequest: approveAll, + availableTools: [], + }); + for (const setting of [undefined, false, true]) { + const resumed = await client.resumeSession(created.sessionId, { + onPermissionRequest: approveAll, + availableTools: [], + allowTranscriptRecovery: setting, + }); + expect(resumed.transcriptRecovery).toEqual(recovery); + expect( + spy.mock.calls.filter(([method]) => method === "session.resume").at(-1)?.[1] + ).toMatchObject({ allowTranscriptRecovery: setting }); + } + } + ); + it("forwards contextTier in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start(); @@ -1788,6 +1842,122 @@ describe("CopilotClient", () => { ); }); + it("deletes an uninitialized cloud session after binary provider validation fails", async () => { + const client = new CopilotClient({ + sessionFs: { + initialCwd: "/", + sessionStatePath: "/state", + conventions: "posix", + capabilities: { binary: true }, + }, + }); + await client.start(); + onTestFinished(() => stopClient(client)); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string) => { + if (method === "session.create") return { sessionId: "cloud-invalid-provider" }; + if (method === "session.delete") return { success: true }; + throw new Error(`Unexpected method: ${method}`); + }); + try { + await expect( + client.createSession({ + onPermissionRequest: approveAll, + cloud: { repository: { owner: "github", name: "copilot-sdk", branch: "main" } }, + createSessionFsProvider: () => ({}) as SessionFsProvider, + }) + ).rejects.toThrow("does not implement readFileBytes"); + expect(spy).toHaveBeenCalledWith("session.delete", { + sessionId: "cloud-invalid-provider", + }); + expect((client as any).sessions.has("cloud-invalid-provider")).toBe(false); + } finally { + spy.mockRestore(); + } + }); + + it("bounds cloud session cleanup when binary provider validation fails", async () => { + const client = new CopilotClient({ + sessionFs: { + initialCwd: "/", + sessionStatePath: "/state", + conventions: "posix", + capabilities: { binary: true }, + }, + }); + await client.start(); + onTestFinished(() => stopClient(client)); + + const deleteStarted = Promise.withResolvers(); + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string) => { + if (method === "session.create") return { sessionId: "cloud-stalled-delete" }; + if (method === "session.delete") { + deleteStarted.resolve(); + return new Promise(() => {}); + } + throw new Error(`Unexpected method: ${method}`); + }); + vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }); + try { + const failure = client + .createSession({ + onPermissionRequest: approveAll, + cloud: { repository: { owner: "github", name: "copilot-sdk", branch: "main" } }, + createSessionFsProvider: () => ({}) as SessionFsProvider, + }) + .catch((error: unknown) => error); + + await deleteStarted.promise; + await vi.advanceTimersByTimeAsync(10_000); + + const error = await failure; + expect(error).toBeInstanceOf(AggregateError); + if (!(error instanceof AggregateError)) throw error; + expect(error.errors).toEqual([ + expect.objectContaining({ + message: expect.stringContaining("does not implement readFileBytes"), + }), + expect.objectContaining({ message: "session.delete timed out after 10000ms" }), + ]); + expect((client as any).sessions.has("cloud-stalled-delete")).toBe(false); + } finally { + vi.useRealTimers(); + spy.mockRestore(); + } + }); + + it("does not register a session when binary provider validation fails during local create or resume", async () => { + const client = new CopilotClient({ + sessionFs: { + initialCwd: "/", + sessionStatePath: "/state", + conventions: "posix", + capabilities: { binary: true }, + }, + }); + await client.start(); + onTestFinished(() => stopClient(client)); + + const invalidProvider = () => ({}) as SessionFsProvider; + await expect( + client.createSession({ + onPermissionRequest: approveAll, + createSessionFsProvider: invalidProvider, + }) + ).rejects.toThrow("does not implement readFileBytes"); + await expect( + client.resumeSession("missing-provider", { + onPermissionRequest: approveAll, + createSessionFsProvider: invalidProvider, + }) + ).rejects.toThrow("does not implement readFileBytes"); + expect((client as any).sessions.size).toBe(0); + }); + it("forwards clientName in session.resume request", async () => { const client = new CopilotClient(); await client.start(); @@ -2439,6 +2609,7 @@ describe("CopilotClient", () => { headers: { Authorization: "Bearer provider-token" }, modelId: "gpt-4o", wireModel: "my-finetune-v3", + modelProvider: "lm_studio", maxPromptTokens: 100_000, maxOutputTokens: 4096, transport: "websockets", @@ -2452,6 +2623,7 @@ describe("CopilotClient", () => { headers: { Authorization: "Bearer provider-token" }, modelId: "gpt-4o", wireModel: "my-finetune-v3", + modelProvider: "lm_studio", maxPromptTokens: 100_000, maxOutputTokens: 4096, transport: "websockets", @@ -2480,6 +2652,7 @@ describe("CopilotClient", () => { headers: { Authorization: "Bearer resume-token" }, modelId: "gpt-4o", wireModel: "my-finetune-v3", + modelProvider: "ollama", maxPromptTokens: 100_000, maxOutputTokens: 4096, transport: "websockets", @@ -2493,6 +2666,7 @@ describe("CopilotClient", () => { headers: { Authorization: "Bearer resume-token" }, modelId: "gpt-4o", wireModel: "my-finetune-v3", + modelProvider: "ollama", maxPromptTokens: 100_000, maxOutputTokens: 4096, transport: "websockets", @@ -2501,6 +2675,44 @@ describe("CopilotClient", () => { spy.mockRestore(); }); + it("forwards named provider model provider in session.create request", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") + return { sessionId: params.sessionId ?? "session-id" }; + throw new Error(`Unexpected method: ${method}`); + }); + + await client.createSession({ + onPermissionRequest: approveAll, + providers: [ + { + name: "local", + baseUrl: "http://localhost:11434/v1", + type: "openai", + modelProvider: "ollama", + }, + ], + models: [{ id: "llama3", provider: "local" }], + }); + + const payload = spy.mock.calls.find(([method]) => method === "session.create")![1] as any; + expect(payload.providers).toEqual([ + expect.objectContaining({ + name: "local", + baseUrl: "http://localhost:11434/v1", + type: "openai", + modelProvider: "ollama", + }), + ]); + spy.mockRestore(); + }); + it("forwards defaultAgent in session.create request", async () => { const client = new CopilotClient(); await client.start(); @@ -3457,6 +3669,185 @@ describe("CopilotClient", () => { }); describe("unexpected disconnection", () => { + it("stops after the runtime exits even when its TCP peer stays open", async () => { + const directory = mkdtempSync(join(tmpdir(), "copilot-surviving-tcp-peer-")); + const runtimePath = join(directory, "runtime.js"); + const client = new CopilotClient({ + connection: RuntimeConnection.forTcp({ path: runtimePath }), + }); + let peer: Socket | undefined; + let serverConnection: MessageConnection | undefined; + // Retain the peer after FIN, independently of the owned runtime process. + const server = createServer({ allowHalfOpen: true }, (socket) => { + peer = socket; + serverConnection = createMessageConnection( + new StreamMessageReader(socket), + new StreamMessageWriter(socket) + ); + serverConnection.onRequest("connect", () => ({ + protocolVersion: getSdkProtocolVersion(), + })); + serverConnection.onRequest("session.create", (params) => ({ + sessionId: params.sessionId, + })); + serverConnection.listen(); + }); + onTestFinished(async () => { + peer?.destroy(); + serverConnection?.dispose(); + await client.forceStop(); + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ); + rmSync(directory, { recursive: true, force: true }); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("Expected a TCP listening address"); + } + writeFileSync( + runtimePath, + `console.log("listening on port ${address.port}"); setInterval(() => {}, 1000);` + ); + await client.createSession({ onPermissionRequest: approveAll }); + const socket = client["socket"]!; + const child = client["cliProcess"]!; + const closed = once(socket, "close", { signal: AbortSignal.timeout(10_000) }); + const exited = once(child, "exit"); + expect(child.kill()).toBe(true); + await exited; + await closed; + expect(client["state"]).toBe("disconnected"); + expect(await client.stop()).toEqual([]); + expect(peer?.destroyed).toBe(false); + }); + + it("disconnects when the runtime exits before an inherited output pipe closes", async () => { + const directory = mkdtempSync(join(tmpdir(), "copilot-inherited-output-")); + const runtimePath = join(directory, "runtime.js"); + const holderPidPath = join(directory, "holder.pid"); + const jsonrpcPath = createRequire(import.meta.url).resolve("vscode-jsonrpc/node"); + writeFileSync( + runtimePath, + ` +const { spawn } = require("node:child_process"); +const { once } = require("node:events"); +const { createMessageConnection, StreamMessageReader, StreamMessageWriter } = require(${JSON.stringify(jsonrpcPath)}); +const holder = spawn(process.execPath, ["-e", "process.send('ready'); setInterval(() => {}, 1000);"], { + stdio: ["ignore", process.stdout, process.stderr, "ipc"], + // Survive the runtime's Windows kill-on-close job while retaining its pipes. + detached: true, + windowsHide: true, +}); +require("node:fs").writeFileSync(${JSON.stringify(holderPidPath)}, String(holder.pid)); +const ready = once(holder, "message"); +const connection = createMessageConnection(new StreamMessageReader(process.stdin), new StreamMessageWriter(process.stdout)); +connection.onRequest("connect", async () => { + await ready; + return { protocolVersion: ${getSdkProtocolVersion()} }; +}); +connection.onRequest("session.create", (params) => ({ sessionId: params.sessionId })); +connection.onRequest("ping", async () => { + await connection.sendNotification("test.pingReceived"); + return new Promise(() => {}); +}); +connection.listen(); +` + ); + const client = new CopilotClient({ + connection: RuntimeConnection.forStdio({ path: runtimePath }), + }); + let closedPromise: Promise | undefined; + const stopHolder = () => { + if (existsSync(holderPidPath)) { + try { + process.kill(Number(readFileSync(holderPidPath, "utf8"))); + } catch (error) { + if ( + !(error instanceof Error && "code" in error && error.code === "ESRCH") + ) { + throw error; + } + } + } + }; + onTestFinished(async () => { + stopHolder(); + await client.forceStop(); + await closedPromise; + rmSync(directory, { recursive: true, force: true }); + }); + await client.start(); + + let signal: AbortSignal | undefined; + let started!: () => void; + const toolStarted = new Promise((resolve) => { + started = resolve; + }); + const session = await client.createSession({ + onPermissionRequest: approveAll, + tools: [ + { + name: "blocked_tool", + description: "blocks until cancelled", + handler: async (_args, invocation) => { + signal = invocation.signal; + started(); + await new Promise((resolve) => + invocation.signal?.addEventListener("abort", () => resolve(), { + once: true, + }) + ); + return "cancelled"; + }, + }, + ], + }); + (session as any)._handleBroadcastEvent({ + type: "external_tool.requested", + data: { + requestId: "inherited-output-request", + sessionId: session.sessionId, + toolCallId: "inherited-output-tool", + toolName: "blocked_tool", + arguments: {}, + }, + }); + await toolStarted; + const pingReceived = new Promise((resolve) => + client["connection"]!.onNotification("test.pingReceived", resolve) + ); + let pingError: unknown; + const ping = client.ping().catch((error) => { + pingError = error; + }); + await pingReceived; + const child = client["cliProcess"]!; + const exited = once(child, "exit"); + closedPromise = once(child, "close"); + let closed = false; + child.once("close", () => { + closed = true; + }); + child.kill(); + await exited; + expect(() => + process.kill(Number(readFileSync(holderPidPath, "utf8")), 0) + ).not.toThrow(); + await vi.waitFor(() => + expect(pingError).toMatchObject({ + message: "Pending response rejected since connection got disposed", + }) + ); + await ping; + expect(closed).toBe(false); + expect(client["state"]).toBe("disconnected"); + expect(signal?.aborted).toBe(true); + stopHolder(); + await closedPromise; + }); + // No child process exists over the in-process (FFI) transport, so this // child-process-kill scenario does not apply there. Covered by the default // (stdio) cell. @@ -3466,6 +3857,12 @@ describe("CopilotClient", () => { const client = new CopilotClient(); await client.start(); onTestFinished(() => stopClient(client)); + vi.spyOn(client["connection"]!, "sendRequest").mockResolvedValueOnce({ + models: [{ id: "before-restart" }], + }); + expect((await client.listModels()).map((model) => model.id)).toEqual([ + "before-restart", + ]); let invocationSignal: AbortSignal | undefined; let toolStarted!: () => void; const started = new Promise((resolve) => { @@ -3515,6 +3912,20 @@ describe("CopilotClient", () => { expect((client as any).state).toBe("disconnected"); expect(invocationSignal?.aborted).toBe(true); }); + await expect(client.listModels()).rejects.toThrow("Client not connected"); + + await client.start(); + expect(client["state"]).toBe("connected"); + await expect(client.ping("restarted")).resolves.toMatchObject({ + message: "pong: restarted", + }); + const sendRequest = vi + .spyOn(client["connection"]!, "sendRequest") + .mockResolvedValueOnce({ models: [{ id: "after-restart" }] }); + expect((await client.listModels()).map((model) => model.id)).toEqual([ + "after-restart", + ]); + expect(sendRequest).toHaveBeenCalledWith("models.list", {}); } ); }); @@ -4346,6 +4757,144 @@ describe("CopilotClient", () => { // No decision returned — the SDK forwards an empty output envelope. expect(response).toEqual({ output: undefined }); }); + + it("routes subagentStart hooks.invoke with normalized parent metadata and additional context", async () => { + const client = new CopilotClient({ autoStart: false }); + + const received: { input: unknown; invocation: { sessionId: string } }[] = []; + const session = new CopilotSession("session-1", {} as any); + session.registerHooks({ + onSubagentStart: async (input, invocation) => { + received.push({ input, invocation }); + return { + additionalContext: "Read the requested file.", + } satisfies SubagentStartHookOutput; + }, + }); + (client as any).sessions.set(session.sessionId, session); + + const response = await (client as any).handleHooksInvoke({ + sessionId: session.sessionId, + hookType: "subagentStart", + input: { + sessionId: "parent-runtime-id", + timestamp: 1700000000000, + cwd: "/repo", + transcriptPath: "/repo/transcript.jsonl", + agentName: "explore", + agentDisplayName: "Explore Agent", + agentDescription: "Reads files", + }, + }); + + expect(received).toEqual([ + { + input: { + sessionId: "parent-runtime-id", + timestamp: new Date(1700000000000), + workingDirectory: "/repo", + transcriptPath: "/repo/transcript.jsonl", + agentName: "explore", + agentDisplayName: "Explore Agent", + agentDescription: "Reads files", + }, + invocation: { sessionId: session.sessionId }, + }, + ]); + expect(response).toEqual({ output: { additionalContext: "Read the requested file." } }); + }); + + it("routes subagentStop hooks.invoke with its response and forwards an explicit allow and rewrite", async () => { + const client = new CopilotClient({ autoStart: false }); + + const received: { input: unknown; invocation: { sessionId: string } }[] = []; + const session = new CopilotSession("session-1", {} as any); + session.registerHooks({ + onSubagentStop: (input, invocation) => { + received.push({ input, invocation }); + return { + decision: "allow", + modifiedResponse: "Reviewed: " + input.response, + } satisfies SubagentStopHookOutput; + }, + }); + (client as any).sessions.set(session.sessionId, session); + + const response = await (client as any).handleHooksInvoke({ + sessionId: session.sessionId, + hookType: "subagentStop", + input: { + sessionId: "parent-runtime-id", + timestamp: 1700000000000, + cwd: "/repo", + transcriptPath: "/repo/transcript.jsonl", + agentId: "read-file", + agentType: "explore", + agentName: "explore", + stopReason: "end_turn", + response: "The file contains a greeting.", + }, + }); + + expect(received).toEqual([ + { + input: { + sessionId: "parent-runtime-id", + timestamp: new Date(1700000000000), + workingDirectory: "/repo", + transcriptPath: "/repo/transcript.jsonl", + agentId: "read-file", + agentType: "explore", + agentName: "explore", + stopReason: "end_turn", + response: "The file contains a greeting.", + }, + invocation: { sessionId: session.sessionId }, + }, + ]); + expect(response).toEqual({ + output: { + decision: "allow", + modifiedResponse: "Reviewed: The file contains a greeting.", + }, + }); + }); + + it("forwards a subagentStop block reason without invoking the top-level agentStop hook", async () => { + const client = new CopilotClient({ autoStart: false }); + + const topLevelStop = vi.fn(); + const session = new CopilotSession("session-1", {} as any); + session.registerHooks({ + onAgentStop: topLevelStop, + onSubagentStop: () => + ({ + decision: "block", + reason: "Read the rest of the file.", + }) satisfies SubagentStopHookOutput, + }); + (client as any).sessions.set(session.sessionId, session); + + const response = await (client as any).handleHooksInvoke({ + sessionId: session.sessionId, + hookType: "subagentStop", + input: { + sessionId: session.sessionId, + timestamp: 1700000000000, + cwd: "/repo", + transcriptPath: "/repo/transcript.jsonl", + agentType: "explore", + agentName: "explore", + stopReason: "end_turn", + response: "First line", + }, + }); + + expect(response).toEqual({ + output: { decision: "block", reason: "Read the rest of the file." }, + }); + expect(topLevelStop).not.toHaveBeenCalled(); + }); }); describe("shutdown", () => { @@ -4388,6 +4937,321 @@ describe("CopilotClient", () => { } ); + it("disconnects when the owned CLI exits before its connection closes", async () => { + const client = new CopilotClient(); + const oldChild = new EventEmitter(); + const child = new EventEmitter(); + const createConnection = () => ({ + onNotification: vi.fn(), + onRequest: vi.fn(), + onClose: vi.fn(), + onError: vi.fn(), + drain: vi.fn(async () => {}), + dispose: vi.fn(), + }); + const oldConnection = createConnection(); + const connection = createConnection(); + const finishDrain = Promise.withResolvers(); + connection.drain.mockImplementation(() => finishDrain.promise); + const sessionDisconnected = vi.fn(); + + (client as any).connection = oldConnection; + (client as any).cliProcess = oldChild; + (client as any).attachConnectionHandlers(); + + (client as any).connection = connection; + (client as any).cliProcess = child; + (client as any).connectionClosed = false; + (client as any).state = "connected"; + (client as any).sessions.set("session-1", { + _markDisconnected: sessionDisconnected, + }); + (client as any).attachConnectionHandlers(); + + oldChild.emit("exit", null, "SIGKILL"); + expect((client as any).state).toBe("connected"); + expect(sessionDisconnected).not.toHaveBeenCalled(); + + child.emit("exit", null, "SIGKILL"); + expect((client as any).connectionClosed).toBe(true); + expect(connection.drain).not.toHaveBeenCalled(); + expect(sessionDisconnected).not.toHaveBeenCalled(); + + const onClose = connection.onClose.mock.calls[0]?.[0]; + expect(onClose).toBeDefined(); + onClose(); + expect(connection.drain).toHaveBeenCalledOnce(); + finishDrain.resolve(); + await vi.waitFor(() => { + expect((client as any).state).toBe("disconnected"); + expect(sessionDisconnected).toHaveBeenCalledTimes(1); + }); + expect((client as any).sessions.size).toBe(0); + onClose(); + expect(sessionDisconnected).toHaveBeenCalledTimes(1); + }); + + it("disconnects after a bounded wait if an exited CLI leaves its reader open", async () => { + const client = new CopilotClient(); + const child = new EventEmitter(); + const connection = { + onNotification: vi.fn(), + onRequest: vi.fn(), + onClose: vi.fn(), + onError: vi.fn(), + drain: vi.fn(async () => {}), + dispose: vi.fn(), + }; + (client as any).connection = connection; + (client as any).cliProcess = child; + (client as any).state = "connected"; + (client as any).attachConnectionHandlers(); + + vi.useFakeTimers(); + try { + child.emit("exit", null, "SIGKILL"); + expect(connection.drain).not.toHaveBeenCalled(); + await vi.advanceTimersByTimeAsync(10_000); + expect(connection.drain).toHaveBeenCalledOnce(); + expect((client as any).state).toBe("disconnected"); + } finally { + vi.useRealTimers(); + } + }); + + it("does not keep the host alive while waiting for an exited CLI's reader", () => { + const client = new CopilotClient(); + const child = new EventEmitter(); + const connection = { + onNotification: vi.fn(), + onRequest: vi.fn(), + onClose: vi.fn(), + onError: vi.fn(), + drain: vi.fn(async () => {}), + dispose: vi.fn(), + }; + (client as any).connection = connection; + (client as any).cliProcess = child; + (client as any).attachConnectionHandlers(); + + const timeoutSpy = vi.spyOn(globalThis, "setTimeout"); + try { + child.emit("exit", null, "SIGKILL"); + const fallback = timeoutSpy.mock.results[0]?.value as NodeJS.Timeout | undefined; + expect(fallback).toBeDefined(); + expect(fallback?.hasRef()).toBe(false); + } finally { + for (const call of timeoutSpy.mock.results) { + clearTimeout(call.value as NodeJS.Timeout); + } + timeoutSpy.mockRestore(); + } + }); + + it("finishes stopping when an exited CLI leaves its TCP peer open", async () => { + const server = createServer({ allowHalfOpen: true }); + let peer: Socket | undefined; + let socket: Socket | undefined; + let timeout: NodeJS.Timeout | undefined; + try { + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + server.off("error", reject); + resolve(); + }); + }); + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("Expected a TCP server address"); + } + const accepted = new Promise((resolve) => { + server.once("connection", resolve); + }); + const connectedSocket = createConnection(address.port, "127.0.0.1"); + socket = connectedSocket; + await new Promise((resolve, reject) => { + connectedSocket.once("connect", resolve); + connectedSocket.once("error", reject); + }); + peer = await accepted; + expect(peer.destroyed).toBe(false); + expect(socket.destroyed).toBe(false); + const client = new CopilotClient({ + autoStart: false, + connection: RuntimeConnection.forTcp(), + }); + (client as any).socket = socket; + (client as any).connectionClosed = true; + (client as any).cliProcess = { exitCode: null, signalCode: "SIGKILL" }; + + const errors = await Promise.race([ + client.stop(), + new Promise((_, reject) => { + timeout = setTimeout( + () => reject(new Error("stop() waited for an exited CLI's TCP peer")), + 5_000 + ); + }), + ]); + expect(errors).toEqual([]); + expect(socket.destroyed).toBe(true); + } finally { + if (timeout) clearTimeout(timeout); + socket?.destroy(); + peer?.destroy(); + if (server.listening) { + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + } + } + }); + + it("tears down an owned CLI when its connected TCP peer holds the socket open", async () => { + const server = createServer({ allowHalfOpen: true }); + let peer: Socket | undefined; + let socket: Socket | undefined; + let stopping: Promise | undefined; + try { + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + server.off("error", reject); + resolve(); + }); + }); + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("Expected a TCP server address"); + } + const accepted = new Promise((resolve) => + server.once("connection", resolve) + ); + socket = createConnection(address.port, "127.0.0.1"); + await new Promise((resolve, reject) => { + socket?.once("connect", resolve); + socket?.once("error", reject); + }); + peer = await accepted; + + const client = new CopilotClient({ + autoStart: false, + connection: RuntimeConnection.forTcp(), + }); + const child = Object.assign(new EventEmitter(), { + exitCode: null as number | null, + signalCode: null as string | null, + kill: vi.fn(() => { + child.exitCode = 0; + child.emit("exit", 0, null); + return true; + }), + }); + (client as any).socket = socket; + (client as any).connectionClosed = false; + (client as any).cliProcess = child; + + vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }); + stopping = client.stop(); + expect(socket.destroyed).toBe(false); + await vi.advanceTimersByTimeAsync(10_000); + expect(socket.destroyed).toBe(true); + expect(await stopping).toEqual([]); + expect(child.kill).toHaveBeenCalledOnce(); + } finally { + socket?.destroy(); + peer?.destroy(); + if (stopping) await stopping; + vi.useRealTimers(); + if (server.listening) { + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + } + } + }); + + it("does not finish starting after the owned CLI exits before a buffered reply arrives", async () => { + const paths = [resolve("plugins/core")]; + const client = new CopilotClient({ builtinPluginDirectories: paths }); + onTestFinished(() => client.forceStop()); + const child = Object.assign(new EventEmitter(), { kill: vi.fn() }); + const registration = Promise.withResolvers(); + const sendRequest = vi.fn((method: string) => { + if (method !== "plugins.builtin.set") + throw new Error(`Unexpected request: ${method}`); + return registration.promise; + }); + const connection = { + onNotification: vi.fn(), + onRequest: vi.fn(), + onClose: vi.fn(), + onError: vi.fn(), + sendRequest, + drain: vi.fn(async () => {}), + dispose: vi.fn(), + }; + vi.spyOn(client as any, "startCLIServer").mockImplementation(async () => { + (client as any).cliProcess = child; + }); + vi.spyOn(client as any, "connectToServer").mockImplementation(async () => { + (client as any).connection = connection; + (client as any).attachConnectionHandlers(); + }); + vi.spyOn(client as any, "verifyProtocolVersion").mockResolvedValue(undefined); + + const starting = client.start(); + await vi.waitFor(() => + expect(sendRequest).toHaveBeenCalledWith("plugins.builtin.set", { paths }) + ); + child.emit("exit", 1, null); + expect((client as any).connectionClosed).toBe(true); + registration.resolve({}); + + await expect(starting).rejects.toThrow("CLI server connection closed during startup"); + expect((client as any).state).not.toBe("connected"); + expect(child.kill).toHaveBeenCalledWith("SIGKILL"); + }); + + it("ignores a previous connection's late close while starting a replacement", async () => { + const client = new CopilotClient(); + onTestFinished(() => client.forceStop()); + const oldChild = Object.assign(new EventEmitter(), { kill: vi.fn() }); + const child = Object.assign(new EventEmitter(), { kill: vi.fn() }); + const createConnection = () => ({ + onNotification: vi.fn(), + onRequest: vi.fn(), + onClose: vi.fn(), + onError: vi.fn(), + drain: vi.fn(async () => {}), + dispose: vi.fn(), + }); + const oldConnection = createConnection(); + const connection = createConnection(); + (client as any).connection = oldConnection; + (client as any).cliProcess = oldChild; + (client as any).attachConnectionHandlers(); + oldChild.emit("exit", 1, null); + await vi.waitFor(() => expect((client as any).state).toBe("disconnected")); + const oldClose = oldConnection.onClose.mock.calls[0]?.[0]; + expect(oldClose).toBeTypeOf("function"); + + vi.spyOn(client as any, "startCLIServer").mockImplementation(async () => { + oldClose(); + (client as any).cliProcess = child; + }); + vi.spyOn(client as any, "connectToServer").mockImplementation(async () => { + (client as any).connection = connection; + (client as any).attachConnectionHandlers(); + }); + vi.spyOn(client as any, "verifyProtocolVersion").mockResolvedValue(undefined); + + await client.start(); + expect((client as any).state).toBe("connected"); + expect((client as any).connectionClosed).toBe(false); + }); + it("requests runtime shutdown when stopping an SDK-owned process", async () => { const client = new CopilotClient(); const calls: string[] = []; diff --git a/nodejs/test/connectors.test.ts b/nodejs/test/connectors.test.ts index b6046a434d..e19d0e7c3e 100644 --- a/nodejs/test/connectors.test.ts +++ b/nodejs/test/connectors.test.ts @@ -1,6 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + import { describe, expect, it, vi } from "vitest"; import type { MessageConnection } from "vscode-jsonrpc"; +import type { + AuthIdentityMetadata, + ConnectorSessionAccount, + ConnectorSessionAccountResult, +} from "../src/index.js"; import { CopilotSession } from "../src/session.js"; describe("session Connector RPC", () => { @@ -135,8 +144,161 @@ describe("session Connector RPC", () => { ], ]); }); + + it("preserves unknown continuation outcomes without treating them as connected", async () => { + const futureResult = { + kind: "future_outcome", + continuationId: "continuation-1", + status: connectorStatus(), + }; + const sendRequest = vi.fn().mockResolvedValue(futureResult); + const session = new CopilotSession("session-1", { + sendRequest, + } as unknown as MessageConnection); + + const result = await session.rpc.connectors.continueConnection({ + continuationId: "continuation-1", + maxAttempts: 1, + pollIntervalMs: 0, + deadlineMs: 1_000, + }); + + expect(result).toEqual(futureResult); + expect(result.kind).not.toBe("connected"); + }); + + it("returns the session account or null and sends an exact targeted reconcile request", async () => { + const authInfo: AuthIdentityMetadata = { + type: "token", + host: "github.com", + login: "alice", + }; + const account: ConnectorSessionAccount = { accountId: "session-account-1", authInfo }; + const sendRequest = vi + .fn() + .mockResolvedValueOnce({ + ...connectorCapabilities(), + sessionAccountSelection: true, + targetedReconcile: true, + }) + .mockResolvedValueOnce(account) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(connectorStatus()); + const session = new CopilotSession("session-1", { + sendRequest, + } as unknown as MessageConnection); + + const capabilities = await session.rpc.connectors.getCapabilities(); + expect(capabilities.availability).toBe("enabled"); + expect(capabilities.sessionAccountSelection).toBe(true); + expect(capabilities.targetedReconcile).toBe(true); + + const selected: ConnectorSessionAccountResult = await session.rpc.connectors.getAccount(); + expect(selected).toEqual(account); + expect(Object.keys(selected!.authInfo).sort()).toEqual(["host", "login", "type"]); + const unavailable: ConnectorSessionAccountResult = + await session.rpc.connectors.getAccount(); + expect(unavailable).toBeNull(); + expect( + await session.rpc.connectors.reconcile({ + accountId: account.accountId, + refreshCatalog: true, + forceConnectorName: "calendar", + }) + ).toEqual(connectorStatus()); + + expect(sendRequest.mock.calls).toEqual([ + ["session.connectors.getCapabilities", { sessionId: "session-1" }], + ["session.connectors.getAccount", { sessionId: "session-1" }], + ["session.connectors.getAccount", { sessionId: "session-1" }], + [ + "session.connectors.reconcile", + { + sessionId: "session-1", + accountId: "session-account-1", + refreshCatalog: true, + forceConnectorName: "calendar", + }, + ], + ]); + }); + + it.each([undefined, false])( + "lets hosts avoid unsupported session-account calls when capabilities are %s", + async (supported) => { + const sendRequest = vi.fn().mockResolvedValue({ + ...connectorCapabilities(), + ...(supported === undefined + ? {} + : { sessionAccountSelection: supported, targetedReconcile: supported }), + }); + const session = new CopilotSession("session-1", { + sendRequest, + } as unknown as MessageConnection); + + const capabilities = await session.rpc.connectors.getCapabilities(); + if ( + capabilities.availability === "enabled" && + capabilities.sessionAccountSelection === true + ) { + await session.rpc.connectors.getAccount(); + } + if ( + capabilities.availability === "enabled" && + capabilities.targetedReconcile === true + ) { + await session.rpc.connectors.reconcile({ + accountId: "account-1", + forceConnectorName: "calendar", + }); + } + + expect(sendRequest.mock.calls).toEqual([ + ["session.connectors.getCapabilities", { sessionId: "session-1" }], + ]); + } + ); + + it("preserves optional catalog presentation metadata without requiring it", async () => { + const entry = { + name: "calendar", + displayName: "Calendar", + status: "connected", + runtimeServerIds: ["connector-calendar"], + }; + const decorated = { + ...entry, + logo: "https://example.com/calendar.svg", + tier: "standard", + releaseTag: "preview", + }; + const sendRequest = vi + .fn() + .mockResolvedValueOnce({ connectors: [decorated], revision: 1, refreshedAtMs: 1 }) + .mockResolvedValueOnce({ connectors: [entry], revision: 2, refreshedAtMs: 2 }); + const session = new CopilotSession("session-1", { + sendRequest, + } as unknown as MessageConnection); + + const catalog = await session.rpc.connectors.list({ accountId: "account-1" }); + expect(catalog.connectors[0]).toEqual(decorated); + const legacy = await session.rpc.connectors.refresh({ accountId: "account-1" }); + expect(legacy.connectors[0]).toEqual(entry); + }); }); +function connectorCapabilities() { + return { + apiVersion: 1, + availability: "enabled", + consentContinuation: true, + opaqueAccountSelection: true, + maxPollAttempts: 30, + maxPollIntervalMs: 2_000, + maxDeadlineMs: 60_000, + }; +} + function connectorStatus() { return { apiVersion: 1, diff --git a/nodejs/test/csharp-codegen.test.ts b/nodejs/test/csharp-codegen.test.ts index 32d905c721..8ff9e3142c 100644 --- a/nodejs/test/csharp-codegen.test.ts +++ b/nodejs/test/csharp-codegen.test.ts @@ -130,6 +130,61 @@ describe("C# additive request entry compatibility", () => { }); }); +describe("C# session event deprecation", () => { + it.each([ + { eventDeprecated: true, dataDeprecated: false }, + { eventDeprecated: false, dataDeprecated: true }, + { eventDeprecated: true, dataDeprecated: true }, + { eventDeprecated: false, dataDeprecated: false }, + ])( + "preserves target-compatible event=$eventDeprecated and payload=$dataDeprecated annotations", + ({ eventDeprecated, dataDeprecated }) => { + const code = generateSessionEventsCode({ + definitions: { + Payload: { + type: "object", + properties: { toolCallId: { type: "string" } }, + ...(dataDeprecated ? { deprecated: true } : {}), + }, + SessionEvent: { + anyOf: [ + { + type: "object", + ...(eventDeprecated ? { deprecated: true } : {}), + properties: { + type: { const: "tool.execution_partial_result" }, + data: { $ref: "#/definitions/Payload" }, + }, + required: ["type", "data"], + }, + ], + }, + }, + }); + const attributes = [ + "[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", + ].join("\n"); + + for (const [name, deprecated] of [ + ["ToolExecutionPartialResultEvent", eventDeprecated], + ["ToolExecutionPartialResultData", dataDeprecated], + ] as const) { + const declaration = `${attributes}\npublic sealed partial class ${name}`; + if (deprecated) expect(code).toContain(declaration); + else expect(code).not.toContain(declaration); + } + expect(code.match(/\[Obsolete\(/g) ?? []).toHaveLength( + 2 * (Number(eventDeprecated) + Number(dataDeprecated)) + ); + } + ); +}); + describe("C# root event payload unions", () => { it.each(["anyOf", "oneOf"] as const)("preserves referenced %s payload variants", (keyword) => { const code = generateSessionEventsCode({ @@ -177,7 +232,93 @@ describe("C# root event payload unions", () => { }); }); +describe("C# session event dispatch", () => { + it.each([ + ["sample.\u00E9", 9], + ["sample.\u{1F600}", 11], + ["sample.'\\", 9], + ])("groups and compares UTF-8 discriminators for %s", (discriminator, byteLength) => { + const code = generateSessionEventsCode({ + definitions: { + SessionEvent: { + anyOf: [ + { + type: "object", + properties: { + type: { const: discriminator }, + data: { type: "object", properties: {} }, + }, + required: ["type", "data"], + }, + ], + }, + }, + }); + + expect(code).toContain(`case ${byteLength}:`); + expect(code).toContain(`type.SequenceEqual(${JSON.stringify(discriminator)}u8)`); + expect(code).toContain("switch (type.Length)"); + expect(code).not.toContain("switch (type)"); + }); + + it("generates concrete dispatch and a schema-derived fallback envelope", () => { + const code = generateSessionEventsCode({ + definitions: { + SessionEvent: { + anyOf: ["sample.first", "sample.second"].map((type) => ({ + type: "object", + properties: { + id: { type: "string", format: "uuid" }, + futureEnvelopeField: { type: "string" }, + type: { const: type }, + data: { type: "object", properties: {} }, + }, + required: ["id", "type", "data"], + })), + }, + }, + }); + + for (const suffix of ["First", "Second"]) { + expect(code).toContain(`type.SequenceEqual("sample.${suffix.toLowerCase()}"u8)`); + expect(code).toContain(`return SessionEventsJsonContext.Default.Sample${suffix}Event;`); + expect(code).toContain(`[JsonSerializable(typeof(Sample${suffix}Event))]`); + } + expect(code).toContain("switch (type.Length)"); + expect(code).toContain("SessionEventJsonConverter.Deserialize(json)"); + expect(code).toContain("ReadEventTypeInfo(ref probe)"); + expect(code).toContain('reader.ValueTextEquals("type"u8)'); + expect(code).toContain("internal sealed class SessionEventEnvelope"); + expect(code).toContain("[JsonSerializable(typeof(SessionEventEnvelope))]"); + expect(code).toContain("Id = Id,"); + expect(code).toContain("FutureEnvelopeField = FutureEnvelopeField,"); + expect(code.match(/public string\? FutureEnvelopeField \{ get; set; \}/g)).toHaveLength(2); + expect(code).toContain( + "JsonSerializer.Serialize(this, SessionEventsJsonContext.Default.SessionEvent)" + ); + }); +}); + describe("C# RPC codegen", () => { + it("emits parameterless MCP list methods", () => { + const code = generateRpcCode({ + session: { + mcp: { + list: { + rpcMethod: "session.mcp.list", + params: null, + }, + }, + }, + }); + expect(code).toContain( + "public async Task ListAsync(CancellationToken cancellationToken = default)" + ); + expect(code).not.toContain("ListWithParamsAsync"); + expect(code).not.toContain("StartServers"); + expect(code).toContain('"session.mcp.list"'); + }); + it("preserves arbitrary JSON handoff settings instead of emitting an empty DTO", () => { const code = generateRpcCode({ server: { diff --git a/nodejs/test/e2e/agent_and_compact_rpc.e2e.test.ts b/nodejs/test/e2e/agent_and_compact_rpc.e2e.test.ts index ba0455282f..bbb446d729 100644 --- a/nodejs/test/e2e/agent_and_compact_rpc.e2e.test.ts +++ b/nodejs/test/e2e/agent_and_compact_rpc.e2e.test.ts @@ -7,6 +7,7 @@ import { describe, expect, it } from "vitest"; import { approveAll } from "../../src/index.js"; import type { CustomAgentConfig } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; describe("Agent Selection RPC", async () => { const { copilotClient: client } = await createSdkTestContext(); @@ -120,6 +121,48 @@ describe("Agent Selection RPC", async () => { await session.disconnect(); }); + it("should emit subagent selected and deselected events", async () => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + customAgents: [ + { + name: "test-agent", + displayName: "Test Agent", + description: "A test agent", + prompt: "You are a test agent.", + }, + ], + }); + const events: Array<{ type: string; data: unknown }> = []; + const unsubscribe = session.on((event) => events.push(event)); + try { + expect((await session.rpc.agent.select({ name: "test-agent" })).agent?.name).toBe( + "test-agent" + ); + await waitForCondition( + () => events.some((event) => event.type === "subagent.selected"), + { + timeoutMessage: `Missing subagent.selected event: ${JSON.stringify(events)}`, + } + ); + const selected = events.find((event) => event.type === "subagent.selected"); + expect(selected?.data).toMatchObject({ + agentName: "test-agent", + agentDisplayName: "Test Agent", + }); + + await session.rpc.agent.deselect(); + await waitForCondition( + () => events.some((event) => event.type === "subagent.deselected"), + { timeoutMessage: `Missing subagent.deselected event: ${JSON.stringify(events)}` } + ); + expect((await session.rpc.agent.getCurrent()).agent).toBeNull(); + } finally { + unsubscribe(); + await session.disconnect(); + } + }); + it("should return empty list when no custom agents configured", async () => { const session = await client.createSession({ onPermissionRequest: approveAll }); diff --git a/nodejs/test/e2e/anthropic_replay.e2e.test.ts b/nodejs/test/e2e/anthropic_replay.e2e.test.ts new file mode 100644 index 0000000000..3b2bad6f2b --- /dev/null +++ b/nodejs/test/e2e/anthropic_replay.e2e.test.ts @@ -0,0 +1,206 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +import { approveAll, CopilotRequestHandler, type CopilotRequestContext } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +const MODEL = "claude-sonnet-5"; +const FIRST_PROMPT = "Use the sql tool to insert the Anthropic replay todo."; +const FOLLOWUP_PROMPT = "Confirm the Anthropic replay todo was inserted."; +const TOOL_ID = "toolu_sdk_replay"; +const TOOL_INPUT = { + description: "Insert Anthropic replay todo", + query: "INSERT INTO todos (id, title, status) VALUES ('sdk-replay', 'Anthropic replay', 'done')", +}; + +type Block = + | { type: "thinking"; thinking: string; signature: string } + | { type: "text"; text: string } + | { type: "tool_use"; id: string; name: string; input: typeof TOOL_INPUT }; + +function messageStream(blocks: Block[], stopReason: "tool_use" | "end_turn"): Response { + const events: Array<[string, unknown]> = [ + [ + "message_start", + { + type: "message_start", + message: { + id: "msg_sdk_replay", + type: "message", + role: "assistant", + model: MODEL, + content: [], + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 5, output_tokens: 1 }, + }, + }, + ], + ]; + for (const [index, block] of blocks.entries()) { + events.push([ + "content_block_start", + { + type: "content_block_start", + index, + content_block: + block.type === "tool_use" + ? { type: "tool_use", id: block.id, name: block.name, input: {} } + : { type: block.type, [block.type === "text" ? "text" : "thinking"]: "" }, + }, + ]); + events.push([ + "content_block_delta", + { + type: "content_block_delta", + index, + delta: + block.type === "thinking" + ? { type: "thinking_delta", thinking: block.thinking } + : block.type === "text" + ? { type: "text_delta", text: block.text } + : { type: "input_json_delta", partial_json: JSON.stringify(block.input) }, + }, + ]); + if (block.type === "thinking") { + events.push([ + "content_block_delta", + { + type: "content_block_delta", + index, + delta: { type: "signature_delta", signature: block.signature }, + }, + ]); + } + events.push(["content_block_stop", { type: "content_block_stop", index }]); + } + events.push( + [ + "message_delta", + { + type: "message_delta", + delta: { stop_reason: stopReason, stop_sequence: null }, + usage: { output_tokens: 10 }, + }, + ], + ["message_stop", { type: "message_stop" }] + ); + return new Response( + events + .map(([event, data]) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`) + .join(""), + { status: 200, headers: { "content-type": "text/event-stream" } } + ); +} + +class AnthropicReplayHandler extends CopilotRequestHandler { + readonly requests: Array<{ messages: Array<{ role: string; content: unknown }> }> = []; + + constructor(private readonly blocks: Block[]) { + super(); + } + + protected override async sendRequest( + request: Request, + _ctx: CopilotRequestContext + ): Promise { + if (!request.url.endsWith("/messages")) { + throw new Error(`Unexpected SDK replay request: ${request.url}`); + } + const body = (await request.json()) as { + messages: Array<{ role: string; content: unknown }>; + }; + this.requests.push(body); + const messages = JSON.stringify(body.messages); + if (messages.includes(FOLLOWUP_PROMPT)) { + return messageStream([{ type: "text", text: "SDK_REPLAY_FOLLOWUP_DONE" }], "end_turn"); + } + if (messages.includes(TOOL_ID)) { + return messageStream([{ type: "text", text: "SDK_REPLAY_TOOL_DONE" }], "end_turn"); + } + return messageStream(this.blocks, "tool_use"); + } +} + +function replayedAssistantContent( + request: { messages: Array<{ role: string; content: unknown }> } | undefined +): unknown[] { + const assistant = request?.messages.find((message) => message.role === "assistant"); + if (!assistant || !Array.isArray(assistant.content)) { + throw new Error("Resumed Anthropic request has no assistant block array"); + } + return assistant.content; +} + +describe("Anthropic replay through SDK sessions", async () => { + const { createClient } = await createSdkTestContext(); + const thinkingOne = { + type: "thinking", + thinking: "First thought.", + signature: "sig-sdk-one", + } as const; + const thinkingTwo = { + type: "thinking", + thinking: "Second thought.", + signature: "sig-sdk-two", + } as const; + const tool = { type: "tool_use", id: TOOL_ID, name: "sql", input: TOOL_INPUT } as const; + + it.each([ + [ + "interleaved", + [ + thinkingOne, + { type: "text", text: "First narration." }, + thinkingTwo, + { type: "text", text: "Second narration." }, + tool, + ], + ], + ["adjacent legacy", [thinkingOne, thinkingTwo, tool]], + ] as const)("preserves %s thinking blocks across a cold SDK resume", async (_name, blocks) => { + const handler = new AnthropicReplayHandler([...blocks]); + const provider = { + type: "anthropic" as const, + baseUrl: "https://anthropic-replay.invalid/v1", + apiKey: "test-provider-key", + modelId: MODEL, + wireModel: MODEL, + }; + const firstClient = createClient({ requestHandler: handler }); + let sessionId: string; + try { + const session = await firstClient.createSession({ + onPermissionRequest: approveAll, + model: MODEL, + provider, + }); + sessionId = session.sessionId; + const result = await session.sendAndWait({ prompt: FIRST_PROMPT }); + expect(result?.data.content).toContain("SDK_REPLAY_TOOL_DONE"); + await session.disconnect(); + } finally { + await firstClient.stop(); + } + + const resumedClient = createClient({ requestHandler: handler }); + try { + const resumed = await resumedClient.resumeSession(sessionId, { + onPermissionRequest: approveAll, + model: MODEL, + provider, + }); + const result = await resumed.sendAndWait({ prompt: FOLLOWUP_PROMPT }); + expect(result?.data.content).toContain("SDK_REPLAY_FOLLOWUP_DONE"); + const followup = handler.requests.findLast((request) => + JSON.stringify(request.messages).includes(FOLLOWUP_PROMPT) + ); + expect(replayedAssistantContent(followup)).toEqual(blocks); + await resumed.disconnect(); + } finally { + await resumedClient.stop(); + } + }); +}); diff --git a/nodejs/test/e2e/auto_tier.e2e.test.ts b/nodejs/test/e2e/auto_tier.e2e.test.ts index f2b5717e60..21401be17d 100644 --- a/nodejs/test/e2e/auto_tier.e2e.test.ts +++ b/nodejs/test/e2e/auto_tier.e2e.test.ts @@ -6,6 +6,7 @@ import { describe, expect, it } from "vitest"; import { approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; import { getNextEventOfType } from "./harness/sdkTestHelper.js"; +import { isByokBackend } from "./harness/testBackend"; /** * The runtime stages an Auto routing preference instead of applying it immediately: a @@ -129,7 +130,9 @@ describe("Auto tier switching", async () => { await overrideClient.stop(); }, 120_000); - it("should commit fast auto tier after successful turn", async () => { + // Activating Auto tiers requires CAPI routing, not a fixed BYOK provider. + it("should commit fast auto tier after successful turn", async ({ skip }) => { + skip(isByokBackend); const session = await client.createSession({ onPermissionRequest: approveAll, model: "auto", @@ -164,7 +167,8 @@ describe("Auto tier switching", async () => { await session.disconnect(); }, 120_000); - it("should preserve effective tier when fast activation fails", async () => { + it("should preserve effective tier when fast activation fails", async ({ skip }) => { + skip(isByokBackend); const session = await client.createSession({ onPermissionRequest: approveAll, model: "auto", diff --git a/nodejs/test/e2e/builtin_tools.e2e.test.ts b/nodejs/test/e2e/builtin_tools.e2e.test.ts index 39900bc7d6..174af3e9ac 100644 --- a/nodejs/test/e2e/builtin_tools.e2e.test.ts +++ b/nodejs/test/e2e/builtin_tools.e2e.test.ts @@ -3,9 +3,10 @@ *--------------------------------------------------------------------------------------------*/ import { writeFile, mkdir } from "fs/promises"; -import { join } from "path"; +import { join, normalize } from "path"; import { describe, expect, it } from "vitest"; import { approveAll } from "../../src/index.js"; +import type { ToolExecutionCompleteEvent } from "../../src/generated/session-events.js"; import { createSdkTestContext } from "./harness/sdkTestContext"; // Built-in tool tests spawn a real CLI subprocess and execute actual shell / @@ -93,8 +94,18 @@ describe("Built-in Tools", async () => { it( "should edit a file successfully", async () => { - await writeFile(join(workDir, "edit_me.txt"), "Hello World\nGoodbye World\n"); + const path = join(workDir, "edit_me.txt"); + await writeFile(path, "Hello World\nGoodbye World\n"); const session = await client.createSession({ onPermissionRequest: approveAll }); + const completions: ToolExecutionCompleteEvent["data"][] = []; + let editToolCallId: string | undefined; + session.on((event) => { + if (event.type === "tool.execution_start" && event.data.toolName === "edit") { + editToolCallId = event.data.toolCallId; + } else if (event.type === "tool.execution_complete") { + completions.push(event.data); + } + }); const msg = await session.sendAndWait( { prompt: "Edit the file 'edit_me.txt': replace 'Hello World' with 'Hi Universe'. Then read it back and tell me its contents.", @@ -102,6 +113,16 @@ describe("Built-in Tools", async () => { SEND_TIMEOUT_MS ); expect(msg?.data.content).toContain("Hi Universe"); + expect(editToolCallId).toBeDefined(); + const editEvent = completions.find((event) => event.toolCallId === editToolCallId); + expect(editEvent?.success).toBe(true); + // Local Windows paths may contain both separator spellings. + expect( + editEvent?.fileEdits?.map((effect) => ({ + ...effect, + path: normalize(effect.path), + })) + ).toEqual([{ path, kind: "edit" }]); }, TEST_TIMEOUT_MS ); @@ -111,7 +132,17 @@ describe("Built-in Tools", async () => { it( "should create a new file", async () => { + const path = join(workDir, "new_file.txt"); const session = await client.createSession({ onPermissionRequest: approveAll }); + const completions: ToolExecutionCompleteEvent["data"][] = []; + let createToolCallId: string | undefined; + session.on((event) => { + if (event.type === "tool.execution_start" && event.data.toolName === "create") { + createToolCallId = event.data.toolCallId; + } else if (event.type === "tool.execution_complete") { + completions.push(event.data); + } + }); const msg = await session.sendAndWait( { prompt: "Create a file called 'new_file.txt' with the content 'Created by test'. Then read it back to confirm.", @@ -119,6 +150,50 @@ describe("Built-in Tools", async () => { SEND_TIMEOUT_MS ); expect(msg?.data.content).toContain("Created by test"); + expect(createToolCallId).toBeDefined(); + const createEvent = completions.find( + (event) => event.toolCallId === createToolCallId + ); + expect(createEvent?.success).toBe(true); + const createFileEdits = createEvent?.fileEdits; + expect( + createFileEdits?.map((effect) => ({ ...effect, path: normalize(effect.path) })) + ).toEqual([{ path, kind: "create" }]); + + const sessionId = session.sessionId; + await client.rpc.sessions.save({ sessionId }); + const eventLog = await session.rpc.eventLog.read({ + max: 100, + waitMs: 0, + types: ["tool.execution_complete"], + }); + const persisted = eventLog.events.find( + (event) => + event.type === "tool.execution_complete" && + event.data.toolCallId === createEvent?.toolCallId + ); + expect(persisted).toMatchObject({ + type: "tool.execution_complete", + data: { + toolCallId: createEvent?.toolCallId, + fileEdits: createFileEdits, + }, + }); + await session.disconnect(); + const resumed = await client.resumeSession(sessionId, { + onPermissionRequest: approveAll, + }); + const replayed = (await resumed.getEvents()).find( + (event) => event.id === persisted?.id + ); + expect(replayed).toMatchObject({ + type: "tool.execution_complete", + data: { + toolCallId: createEvent?.toolCallId, + fileEdits: createFileEdits, + }, + }); + await resumed.disconnect(); }, TEST_TIMEOUT_MS ); diff --git a/nodejs/test/e2e/client.e2e.test.ts b/nodejs/test/e2e/client.e2e.test.ts index a529ea8e4c..e4e826a922 100644 --- a/nodejs/test/e2e/client.e2e.test.ts +++ b/nodejs/test/e2e/client.e2e.test.ts @@ -1,7 +1,8 @@ import { ChildProcess } from "child_process"; +import type { Socket } from "node:net"; import { describe, expect, it, onTestFinished, vi } from "vitest"; import { approveAll, CopilotClient, RuntimeConnection } from "../../src/index.js"; -import { isInProcessTransport } from "./harness/sdkTestContext.js"; +import { createSdkTestContext, isInProcessTransport } from "./harness/sdkTestContext.js"; function onTestFinishedStop(client: CopilotClient) { onTestFinished(async () => { @@ -87,25 +88,82 @@ describe("Client", () => { // because the JSON-RPC logic is still trying to write to stdin after // the process has exited. const client = new CopilotClient({ connection: RuntimeConnection.forTcp() }); + let phase = "createSession"; + const startedAt = Date.now(); + let phaseStartedAt = startedAt; + const phaseTimes = ["createSession=0ms"]; + let stopSocket: Socket | null = null; + let stopProcess: ChildProcess | null = null; + const logPendingPhase = (reason: string) => { + const status = client as unknown as { + state: string; + connectionClosed: boolean; + connection: unknown; + socket: Socket | null; + cliProcess: ChildProcess | null; + }; + const socket = stopSocket ?? status.socket; + const child = stopProcess ?? status.cliProcess; + console.warn( + `${reason}: phase=${phase} for ${Date.now() - phaseStartedAt}ms, ` + + `phases=${phaseTimes.join(", ")}, state=${status.state}, ` + + `connectionClosed=${status.connectionClosed}, connectionActive=${status.connection !== null}, ` + + `child exitCode=${child?.exitCode}, signalCode=${child?.signalCode}, ` + + `socket destroyed=${socket?.destroyed}, readyState=${socket?.readyState}` + ); + }; + let diagnosticTimer = setTimeout(() => logPendingPhase("Still waiting"), 20_000); + const startPhase = (nextPhase: string) => { + clearTimeout(diagnosticTimer); + phase = nextPhase; + phaseStartedAt = Date.now(); + phaseTimes.push(`${phase}=${phaseStartedAt - startedAt}ms`); + diagnosticTimer = setTimeout(() => logPendingPhase("Still waiting"), 20_000); + }; + let completed = false; + onTestFinished(async () => { + clearTimeout(diagnosticTimer); + if (!completed) logPendingPhase("Test ended before completion"); + await client.forceStop(); + }); await client.createSession({ onPermissionRequest: approveAll }); - // Kill the server processto force cleanup to fail + startPhase("child exit"); + // Kill the server process to force cleanup to fail // eslint-disable-next-line @typescript-eslint/no-explicit-any const cliProcess = (client as any).cliProcess as ChildProcess; expect(cliProcess).toBeDefined(); - cliProcess.kill("SIGKILL"); + expect(cliProcess.kill("SIGKILL")).toBe(true); await vi.waitFor( () => { - expect((client as unknown as { state: string }).state).toBe("disconnected"); + const socket = ( + client as unknown as { + socket: { destroyed: boolean; readyState: string } | null; + } + ).socket; + expect( + (client as unknown as { state: string }).state, + `After SIGKILL: child exitCode=${cliProcess.exitCode}, signalCode=${cliProcess.signalCode}, ` + + `socket destroyed=${socket?.destroyed}, readyState=${socket?.readyState}` + ).toBe("disconnected"); }, { timeout: 10_000 } ); + stopSocket = (client as unknown as { socket: Socket | null }).socket; + stopProcess = cliProcess; + phaseTimes.push( + `socketAtStop=${stopSocket?.readyState}/${stopSocket?.destroyed}, ` + + `childAtStop=${cliProcess.exitCode}/${cliProcess.signalCode}` + ); + startPhase("client.stop()"); const errors = await client.stop(); + clearTimeout(diagnosticTimer); if (errors.length > 0) { expect(errors[0].message).toContain("Failed to disconnect session"); } + completed = true; }, // Generous timeout: client.stop() must wait for session.detach to time out // when the server process is dead. The default 30s can flake on slow CI under load. @@ -157,31 +215,25 @@ describe("Client", () => { await client.stop(); }); - it("should list models when authenticated", async () => { - const client = new CopilotClient(); - onTestFinishedStop(client); + describe("model catalog", async () => { + const { copilotClient: client } = await createSdkTestContext(); - await client.start(); + it("should list models when authenticated", async () => { + await client.start(); - const authStatus = await client.getAuthStatus(); - if (!authStatus.isAuthenticated) { - // Skip if not authenticated - models.list requires auth - await client.stop(); - return; - } + const authStatus = await client.getAuthStatus(); + expect(authStatus.isAuthenticated).toBe(true); - const models = await client.listModels(); - expect(Array.isArray(models)).toBe(true); - if (models.length > 0) { + const models = await client.listModels(); + expect(Array.isArray(models)).toBe(true); + expect(models.length).toBeGreaterThan(0); const model = models[0]; expect(model.id).toBeDefined(); expect(model.name).toBeDefined(); expect(model.capabilities).toBeDefined(); expect(model.capabilities.supports).toBeDefined(); expect(model.capabilities.limits).toBeDefined(); - } - - await client.stop(); + }); }); it.skipIf(isInProcessTransport)("should report error when CLI fails to start", async () => { diff --git a/nodejs/test/e2e/client_options.e2e.test.ts b/nodejs/test/e2e/client_options.e2e.test.ts index b32965ec0f..544ab712e7 100644 --- a/nodejs/test/e2e/client_options.e2e.test.ts +++ b/nodejs/test/e2e/client_options.e2e.test.ts @@ -206,7 +206,7 @@ function getArray(value: unknown): unknown[] { } describe("Client options", async () => { - const { copilotClient: defaultClient, env, workDir } = await createSdkTestContext(); + const { createClient, env, workDir } = await createSdkTestContext(); it("createSession starts the client lazily", async () => { const client = new CopilotClient({ @@ -260,11 +260,7 @@ describe("Client options", async () => { fs.mkdirSync(clientCwd, { recursive: true }); fs.writeFileSync(path.join(clientCwd, "marker.txt"), "I am in the client cwd"); - // Reference defaultClient to keep the shared test context (and its CAPI proxy/env) - // alive for the duration of this test; we deliberately spin up a fresh client with - // a custom cwd to assert that the custom cwd is honored. - void defaultClient; - const client = new CopilotClient({ + const client = createClient({ workingDirectory: clientCwd, env, connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), diff --git a/nodejs/test/e2e/commit_trailer_resume.e2e.test.ts b/nodejs/test/e2e/commit_trailer_resume.e2e.test.ts new file mode 100644 index 0000000000..ac30d3817a --- /dev/null +++ b/nodejs/test/e2e/commit_trailer_resume.e2e.test.ts @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it, onTestFinished } from "vitest"; +import { approveAll, RuntimeConnection, type SessionConfig } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +describe("Commit trailer resume", async () => { + const { createClient, openAiEndpoint } = await createSdkTestContext(); + + it.each(["options update", "cold resume"])( + "allows coauthor opt-in after an initial opt-out via %s", + async (transition) => { + const config: SessionConfig = { + model: "claude-sonnet-5", + coauthorEnabled: false, + featureFlags: { SESSION_TRAILER: true }, + onPermissionRequest: approveAll, + availableTools: [], + skipCustomInstructions: true, + }; + const first = createClient({ connection: RuntimeConnection.forStdio() }); + onTestFinished(() => first.stop()); + let session = await first.createSession(config); + const sessionId = session.sessionId; + const disabledResponse = await session.sendAndWait({ + prompt: "Reply with exactly COAUTHOR_DISABLED_OK.", + }); + expect(disabledResponse?.data.content).toContain("COAUTHOR_DISABLED_OK"); + const disabledRequest = (await openAiEndpoint.getExchanges()).at(-1); + const disabledSystem = disabledRequest?.request.messages.find( + (message) => message.role === "system" + )?.content; + expect(typeof disabledSystem).toBe("string"); + expect(disabledSystem).not.toContain(""); + + if (transition === "options update") { + await session.rpc.options.update({ coauthorEnabled: true }); + } else { + await session.disconnect(); + await first.stop(); + const second = createClient({ connection: RuntimeConnection.forStdio() }); + onTestFinished(() => second.stop()); + session = await second.resumeSession(sessionId, { + ...config, + coauthorEnabled: true, + }); + } + + const enabledResponse = await session.sendAndWait({ + prompt: "Reply with exactly COAUTHOR_ENABLED_OK.", + }); + expect(enabledResponse?.data.content).toContain("COAUTHOR_ENABLED_OK"); + const enabledRequest = (await openAiEndpoint.getExchanges()).at(-1); + const enabledSystem = enabledRequest?.request.messages.find( + (message) => message.role === "system" + )?.content; + expect(enabledSystem).toContain("Co-authored-by: Copilot"); + expect(enabledSystem).toContain(`Copilot-Session: ${sessionId}`); + await session.disconnect(); + } + ); + + it.each([false, true])( + "restores uncustomized attribution across cold resume (quoted tags: %s)", + async (quotedTags) => { + const config: SessionConfig = { + model: "claude-sonnet-5", + coauthorEnabled: true, + featureFlags: { SESSION_TRAILER: true }, + onPermissionRequest: approveAll, + availableTools: [], + skipCustomInstructions: true, + systemMessage: { + mode: "customize", + sections: { + custom_instructions: { + action: "append", + content: quotedTags + ? "Quoted example: before" + : "", + }, + runtime_instructions: { + action: (content: string) => + content + .replace("Co-authored-by: ", "Co-authored-by: Team ") + .replace( + "", + "TRAILER_POLICY_ONCE\n" + ), + }, + last_instructions: { + action: "append", + content: quotedTags + ? "Quoted example: after" + : "", + }, + }, + }, + }; + const first = createClient({ connection: RuntimeConnection.forStdio() }); + onTestFinished(() => first.stop()); + const session = await first.createSession(config); + const sessionId = session.sessionId; + const response = await session.sendAndWait({ + prompt: "Reply with exactly TRAILER_INITIAL_OK.", + }); + expect(response?.data.content).toContain("TRAILER_INITIAL_OK"); + const before = (await openAiEndpoint.getExchanges()).at(-1); + const initialSystem = before?.request.messages.find( + (message) => message.role === "system" + )?.content; + expect(typeof initialSystem).toBe("string"); + const initialBlocks = String(initialSystem).match( + /[\s\S]*?<\/git_commit_trailer>/g + ); + expect(initialBlocks).toHaveLength(quotedTags ? 3 : 1); + const generated = initialBlocks?.[quotedTags ? 1 : 0]; + expect(generated).toContain(`Copilot-Session: ${sessionId}`); + expect(generated).toContain("Co-authored-by: Team Copilot"); + expect(generated?.match(/TRAILER_POLICY_ONCE/g)).toHaveLength(1); + await session.disconnect(); + await first.stop(); + + const second = createClient({ connection: RuntimeConnection.forStdio() }); + onTestFinished(() => second.stop()); + const resumed = await second.resumeSession(sessionId, config); + const resumedResponse = await resumed.sendAndWait({ + prompt: "Reply with exactly TRAILER_RESUMED_OK.", + }); + expect(resumedResponse?.data.content).toContain("TRAILER_RESUMED_OK"); + const after = (await openAiEndpoint.getExchanges()).at(-1); + const resumedSystem = after?.request.messages.find( + (message) => message.role === "system" + )?.content; + expect( + String(resumedSystem).match(/[\s\S]*?<\/git_commit_trailer>/g) + ).toEqual(initialBlocks); + await resumed.disconnect(); + await second.stop(); + + const third = createClient({ connection: RuntimeConnection.forStdio() }); + onTestFinished(() => third.stop()); + const optedOut = await third.resumeSession(sessionId, { + ...config, + coauthorEnabled: false, + }); + const finalResponse = await optedOut.sendAndWait({ + prompt: "Reply with exactly TRAILER_OPT_OUT_OK.", + }); + expect(finalResponse?.data.content).toContain("TRAILER_OPT_OUT_OK"); + const finalRequest = (await openAiEndpoint.getExchanges()).at(-1); + const finalSystem = finalRequest?.request.messages.find( + (message) => message.role === "system" + )?.content; + expect(finalSystem).not.toContain(`Copilot-Session: ${sessionId}`); + expect(finalSystem).not.toContain("Co-authored-by:"); + await optedOut.disconnect(); + } + ); +}); diff --git a/nodejs/test/e2e/compaction.e2e.test.ts b/nodejs/test/e2e/compaction.e2e.test.ts index a74878101d..04de4d1d4a 100644 --- a/nodejs/test/e2e/compaction.e2e.test.ts +++ b/nodejs/test/e2e/compaction.e2e.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { approveAll, type CopilotSession, type SessionEvent } from "../../src/index.js"; -import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { createSdkTestContext, isCI } from "./harness/sdkTestContext.js"; const compactionTimeoutMs = 60_000; @@ -35,7 +35,7 @@ function getNextSessionEvent( } describe("Compaction", async () => { - const { copilotClient: client } = await createSdkTestContext(); + const { copilotClient: client, openAiEndpoint } = await createSdkTestContext(); it("should trigger compaction with low threshold and emit events", async () => { // Create session with very low compaction thresholds to trigger compaction quickly @@ -84,7 +84,23 @@ describe("Compaction", async () => { expect(startEvent.data.conversationTokens ?? 0).toBeGreaterThan(0); expect(completeEvent.data.success).toBe(true); expect(completeEvent.data.compactionTokensUsed).toBeDefined(); - expect(completeEvent.data.compactionTokensUsed?.inputTokens ?? 0).toBeGreaterThan(0); + // Replay has no prompt usage; recording must preserve the provider's value or absence. + if (isCI) { + expect(completeEvent.data.compactionTokensUsed?.inputTokens ?? 0).toBe(0); + } else { + const exchanges = await openAiEndpoint.getExchanges(); + const compactions = exchanges.filter( + ({ compactionUsage }) => + compactionUsage && compactionUsage.summary === completeEvent.data.summaryContent + ); + expect( + compactions, + "Expected one provider compaction chain for the completed summary" + ).toHaveLength(1); + expect(completeEvent.data.compactionTokensUsed?.inputTokens).toBe( + compactions[0].compactionUsage?.inputTokens + ); + } const summary = (completeEvent.data.summaryContent ?? "").toLowerCase(); expect(summary).toContain(""); expect(summary).toContain(""); diff --git a/nodejs/test/e2e/connectors.e2e.test.ts b/nodejs/test/e2e/connectors.e2e.test.ts new file mode 100644 index 0000000000..3a7717e71a --- /dev/null +++ b/nodejs/test/e2e/connectors.e2e.test.ts @@ -0,0 +1,433 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { createServer, type IncomingMessage, type Server } from "node:http"; +import { createServer as createHttpsServer, type Server as HttpsServer } from "node:https"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import type { ConnectorSessionAccount, ConnectorStatus } from "../../src/index.js"; +import { approveAll, CopilotClient, RuntimeConnection } from "../../src/index.js"; +import { createIdentityForHost, generateCA, type CaData } from "../../../test/harness/certUtils.js"; +import { createSdkTestContext, isInProcessTransport } from "./harness/sdkTestContext.js"; + +const SESSION_TOKEN = "ghu_sdk_connector_session"; +const MCP_PROTOCOL_VERSION = "2025-03-26"; +const CONNECTOR_FLAGS = { MANAGED_MCP_SERVERS: true, CONNECTORS: true }; + +/** + * Exercises the session-owned Connector account surface through the public SDK + * client against the built runtime: a credential-owned session selects its own + * GitHub identity with `connectors.getAccount()`, and a targeted `reconcile` + * replaces only the named Connector's MCP projection. The fixture stands in for + * the GitHub API and the Connector service; no model request is made, so the + * runtime's own RPC results are the observable completion boundary. + */ +// The in-process transport shares this test process's ambient environment, so it cannot +// take the per-client CA bundle the Connector MCP fixtures are signed with. The default +// stdio cell covers the behavior. +describe.skipIf(isInProcessTransport)("Session-owned Connector accounts", async () => { + const { env, workDir } = await createSdkTestContext(); + + // Every fixture handle starts unset and cleanup is registered before any of them is + // acquired, so a failed startup still releases whatever was already listening. + let fixtureDir: string | undefined; + let mail: ConnectorMcpFixture | undefined; + let relocatedMail: ConnectorMcpFixture | undefined; + let calendar: ConnectorMcpFixture | undefined; + let ordinary: ConnectorMcpFixture | undefined; + let service: ConnectorServiceFixture | undefined; + let client: CopilotClient | undefined; + + afterAll(async () => { + await client?.stop().catch(() => undefined); + await Promise.allSettled( + [service, mail, relocatedMail, calendar, ordinary].map((fixture) => fixture?.close()) + ); + if (fixtureDir) { + rmSync(fixtureDir, { recursive: true, force: true }); + } + }); + + beforeAll(async () => { + // Connector-owned MCP endpoints must be HTTPS, so the fixtures are signed by a CA + // appended to the harness bundle the runtime already trusts. + const ca = generateCA(); + fixtureDir = mkdtempSync(join(tmpdir(), "sdk-connectors-")); + mail = await startConnectorMcpServer("mail", ca); + relocatedMail = await startConnectorMcpServer("mail-relocated", ca); + calendar = await startConnectorMcpServer("calendar", ca); + ordinary = await startConnectorMcpServer("ordinary", ca); + service = await startConnectorService({ + mailUrl: mail.url, + calendarUrl: calendar.url, + }); + + const caBundlePath = join(fixtureDir, "ca-bundle.pem"); + writeFileSync( + caBundlePath, + [readCaBundle(env.NODE_EXTRA_CA_CERTS), ca.certPem].filter(Boolean).join("\n") + ); + // Point both the GitHub API and the Connector service at the fixture so the session's + // own credential is the only identity the catalog can be read with. + client = new CopilotClient({ + workingDirectory: workDir, + env: { + ...env, + COPILOT_DEBUG_GITHUB_API_URL: service.url, + NODE_EXTRA_CA_CERTS: caBundlePath, + SSL_CERT_FILE: caBundlePath, + REQUESTS_CA_BUNDLE: caBundlePath, + CURL_CA_BUNDLE: caBundlePath, + }, + logLevel: "error", + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + }); + }); + + function started() { + if (!client || !service || !mail || !relocatedMail || !calendar || !ordinary) { + throw new Error("Connector fixtures did not start"); + } + return { client, service, mail, relocatedMail, calendar, ordinary }; + } + + it( + "selects a credential-free session account and reconciles only the targeted Connector", + { timeout: 120_000 }, + async () => { + const { client, service, mail, relocatedMail, calendar, ordinary } = started(); + const session = await client.createSession({ + onPermissionRequest: approveAll, + gitHubToken: SESSION_TOKEN, + featureFlags: CONNECTOR_FLAGS, + disabledMcpServers: ["github-mcp-server"], + mcpServers: { + ordinary: { type: "http", url: `${ordinary.url}/mcp`, tools: ["*"] }, + }, + }); + + try { + const capabilities = await session.rpc.connectors.getCapabilities(); + expect(capabilities).toMatchObject({ + availability: "enabled", + sessionAccountSelection: true, + targetedReconcile: true, + }); + + const selected: ConnectorSessionAccount | null = + await session.rpc.connectors.getAccount(); + expect(selected).not.toBeNull(); + const account = selected!; + expect(typeof account.accountId).toBe("string"); + expect(account.accountId.length).toBeGreaterThan(0); + expect(account.authInfo).toEqual({ + type: "token", + host: "https://github.com", + login: "octocat", + }); + // The selection is an opaque routing identifier: no credential may reach the client. + expect(JSON.stringify(account)).not.toContain(SESSION_TOKEN); + + // getAccount alone must not pin the session or touch the Connector service. + expect(service.catalogReads).toBe(0); + expect((await session.rpc.connectors.getStatus()).accountId).toBeUndefined(); + + const connected = await session.rpc.connectors.reconcile({ + accountId: account.accountId, + refreshCatalog: true, + }); + expectConnected(connected, "mail"); + expectConnected(connected, "calendar"); + expect(service.catalogReads).toBeGreaterThan(0); + expect(JSON.stringify(connected)).not.toContain(SESSION_TOKEN); + expect(mail.authorizationHeaders.length).toBeGreaterThan(0); + + // Relocate only `mail` in the authoritative catalog, then reconcile it by name. + service.setMailUrl(relocatedMail.url); + const before = { + mail: mail.initializations, + relocatedMail: relocatedMail.initializations, + calendar: calendar.initializations, + ordinary: ordinary.initializations, + }; + const targeted = await session.rpc.connectors.reconcile({ + accountId: account.accountId, + refreshCatalog: true, + forceConnectorName: "mail", + }); + expectConnected(targeted, "mail"); + expect(relocatedMail.initializations).toBe(before.relocatedMail + 1); + expect(mail.initializations).toBe(before.mail); + expect(calendar.initializations).toBe(before.calendar); + expect(ordinary.initializations).toBe(before.ordinary); + + // An unknown Connector name is rejected rather than silently reconciling everything. + await expect( + session.rpc.connectors.reconcile({ + accountId: account.accountId, + forceConnectorName: "unknown", + }) + ).rejects.toThrow(); + } finally { + await session.disconnect(); + } + } + ); +}); + +function expectConnected(status: ConnectorStatus, connectorName: string): void { + expect(status.runtimeServers).toEqual( + expect.arrayContaining([expect.objectContaining({ connectorName, status: "connected" })]) + ); +} + +interface ConnectorServiceFixture { + readonly url: string; + readonly catalogReads: number; + setMailUrl(url: string): void; + close(): Promise; +} + +/** + * Minimal GitHub API and Connector service the runtime can read a catalog from. + * Only the session credential is accepted, so a catalog read proves the session's + * own identity was used. + */ +async function startConnectorService({ + mailUrl, + calendarUrl, +}: { + mailUrl: string; + calendarUrl: string; +}): Promise { + let currentMailUrl = mailUrl; + let catalogReads = 0; + let selfUrl = ""; + const server = createServer((request, response) => { + const authorization = request.headers.authorization ?? ""; + const token = authorization.slice(authorization.indexOf(" ") + 1); + response.setHeader("content-type", "application/json"); + if (request.url?.startsWith("/copilot_internal/user")) { + if (token !== SESSION_TOKEN) { + response.writeHead(401).end(JSON.stringify({ message: "Bad credentials" })); + return; + } + response.end( + JSON.stringify({ + login: "octocat", + copilot_plan: "individual_pro", + is_mcp_enabled: true, + endpoints: { api: selfUrl }, + }) + ); + return; + } + if (request.url === "/copilot-connectors/api/v1/plugins") { + if (token !== SESSION_TOKEN) { + response.writeHead(401).end(JSON.stringify({ message: "Bad credentials" })); + return; + } + catalogReads++; + response.end( + JSON.stringify({ + plugins: [ + connectorPlugin("mail", currentMailUrl), + connectorPlugin("calendar", calendarUrl), + ], + }) + ); + return; + } + response.writeHead(404).end("{}"); + }); + const url = await listen(server); + selfUrl = url; + return { + url, + get catalogReads() { + return catalogReads; + }, + setMailUrl(next: string) { + currentMailUrl = next; + }, + close: () => closeServer(server), + }; +} + +function connectorPlugin(name: string, url: string) { + return { + name, + logo: "https://images.example/connector.svg", + metadata: { displayName: name, tier: "standard", releaseTag: "preview" }, + connection: { status: "connected" }, + mcpServers: { mcpServers: { [name]: { type: "http", url: `${url}/mcp` } } }, + }; +} + +interface ConnectorMcpFixture { + readonly url: string; + readonly initializations: number; + readonly authorizationHeaders: string[]; + close(): Promise; +} + +/** Hand-rolled Streamable HTTP MCP endpoint: enough for the runtime to initialize and list one tool. */ +async function startConnectorMcpServer(name: string, ca: CaData): Promise { + let initializations = 0; + const authorizationHeaders: string[] = []; + const identity = createIdentityForHost("127.0.0.1", ca); + const server = createHttpsServer( + { key: identity.keyPem, cert: identity.certPem }, + (request, response) => { + void (async () => { + if (request.url !== "/mcp" || request.method !== "POST") { + response.writeHead(404, { "content-type": "application/json" }).end("{}"); + return; + } + if (request.headers.authorization) { + authorizationHeaders.push(request.headers.authorization); + } + const body = await readBody(request); + let message: unknown; + try { + message = JSON.parse(body); + } catch { + response.writeHead(400, { "content-type": "application/json" }).end("{}"); + return; + } + const messages = Array.isArray(message) ? message : [message]; + const replies = messages + .map((item) => + handleMcpMessage(item as McpMessage, name, () => initializations++) + ) + .filter((item) => item !== undefined); + if (replies.length === 0) { + response.writeHead(202, { "mcp-session-id": `${name}-session` }).end(); + return; + } + response.writeHead(200, { + "content-type": "application/json", + "mcp-session-id": `${name}-session`, + }); + response.end(JSON.stringify(Array.isArray(message) ? replies : replies[0])); + })().catch(() => { + if (!response.headersSent) { + response.writeHead(500, { "content-type": "application/json" }); + } + response.end("{}"); + }); + } + ); + const url = await listen(server, "https"); + return { + url, + get initializations() { + return initializations; + }, + authorizationHeaders, + close: () => closeServer(server), + }; +} + +interface McpMessage { + id?: unknown; + method?: string; + params?: { protocolVersion?: string }; +} + +function handleMcpMessage( + message: McpMessage, + name: string, + onInitialize: () => void +): unknown | undefined { + if (!message || typeof message !== "object" || message.id === undefined) { + return undefined; + } + switch (message.method) { + case "initialize": + onInitialize(); + return { + jsonrpc: "2.0", + id: message.id, + result: { + protocolVersion: message.params?.protocolVersion ?? MCP_PROTOCOL_VERSION, + capabilities: { tools: {} }, + serverInfo: { name, version: "1.0.0" }, + }, + }; + case "tools/list": + return { + jsonrpc: "2.0", + id: message.id, + result: { + tools: [ + { + name: `${name}_ping`, + description: `Returns the ${name} fixture principal.`, + inputSchema: { type: "object", properties: {} }, + }, + ], + }, + }; + case "tools/call": + return { + jsonrpc: "2.0", + id: message.id, + result: { content: [{ type: "text", text: name }], isError: false }, + }; + default: + return { + jsonrpc: "2.0", + id: message.id, + error: { code: -32601, message: `Method not found: ${message.method}` }, + }; + } +} + +function readBody(request: IncomingMessage): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + request.on("data", (chunk: Buffer) => chunks.push(chunk)); + request.on("error", reject); + request.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); + }); +} + +function readCaBundle(path: string | undefined): string { + if (!path) { + return ""; + } + try { + return readFileSync(path, "utf8"); + } catch { + return ""; + } +} + +async function listen( + server: Server | HttpsServer, + scheme: "http" | "https" = "http" +): Promise { + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + server.off("error", reject); + resolve(); + }); + }); + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("Connector fixture did not bind a TCP port"); + } + return `${scheme}://127.0.0.1:${address.port}`; +} + +function closeServer(server: Server | HttpsServer): Promise { + return new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + server.closeAllConnections(); + }); +} diff --git a/nodejs/test/e2e/copilot_request_handler.e2e.test.ts b/nodejs/test/e2e/copilot_request_handler.e2e.test.ts index 04bccb7e08..af79d84ae3 100644 --- a/nodejs/test/e2e/copilot_request_handler.e2e.test.ts +++ b/nodejs/test/e2e/copilot_request_handler.e2e.test.ts @@ -10,9 +10,11 @@ import { approveAll, CopilotRequestHandler, CopilotWebSocketForwarder, + defineTool, type CopilotRequestContext, } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; const HTTP_TEXT = "OK from synthetic HTTP upstream."; const WS_TEXT = "OK from synthetic WS upstream."; @@ -32,9 +34,13 @@ async function startFakeUpstream(): Promise<{ url: string; server: HttpServer; wsRequestCount: () => number; + wsBodies: () => Array>; + httpBodies: () => Array>; close: () => Promise; }> { let wsRequests = 0; + const wsBodies: Array> = []; + const httpBodies: Array> = []; const httpServer = createServer((req, res) => { const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`); @@ -66,6 +72,30 @@ async function startFakeUpstream(): Promise<{ }, }, }, + { + id: "gpt-6-sol", + name: "GPT-6 Sol", + object: "model", + vendor: "OpenAI", + version: "1", + preview: false, + model_picker_enabled: true, + supported_endpoints: ["/responses", "ws:/responses"], + capabilities: { + type: "chat", + family: "gpt-6-sol", + tokenizer: "o200k_base", + limits: { + max_context_window_tokens: 200000, + max_output_tokens: 8192, + }, + supports: { + streaming: true, + tool_calls: true, + parallel_tool_calls: true, + }, + }, + }, ], }); return; @@ -79,15 +109,20 @@ async function startFakeUpstream(): Promise<{ return; } if (url.pathname.endsWith("/responses") && req.method === "POST") { - // Single-shot HTTP inference (e.g. title generation). SSE - // events the `responses-client.ts` reducer accepts. drainBody(req) - .then(() => { + .then((raw) => { + const body = JSON.parse(raw.toString()) as Record; + httpBodies.push(body); + const events = buildResponsesEvents(HTTP_TEXT, "resp_stub_http"); + if (body.stream !== true) { + sendJson(res, 200, events.at(-1)!.response); + return; + } res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", }); - for (const event of buildResponsesEvents(HTTP_TEXT, "resp_stub_http")) { + for (const event of events) { res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`); } res.end(); @@ -106,13 +141,73 @@ async function startFakeUpstream(): Promise<{ wss.on("connection", (socket) => { socket.on("message", (raw) => { wsRequests++; - // For each `response.create` request the runtime sends, - // answer with the ordered `/responses` event objects — one - // event per outbound WS message, raw JSON (NOT SSE-framed). - for (const event of buildResponsesEvents(WS_TEXT, "resp_stub_ws")) { + const body = JSON.parse(raw.toString()) as Record; + wsBodies.push(body); + const input = JSON.stringify(body.input); + const id = `resp_stub_ws_${wsRequests}`; + const positionedUpdate = + Array.isArray(body.input) && + body.input.some((item: { type?: string }) => item.type === "additional_tools"); + const rejection = { + type: "error", + error: { + code: "bad_request", + type: "websocket_error", + message: "additional_tools rejected", + }, + }; + const restricted = + (body.tool_choice as { type?: string } | undefined)?.type === "allowed_tools"; + let events: Array>; + if (restricted && input.includes("SDK_ALLOWLIST_REJECT_BEFORE")) { + events = [ + { + ...rejection, + error: { + ...rejection.error, + code: "invalid_request_body", + message: "allowed_tools rejected", + }, + }, + ]; + } else if (restricted && input.includes("SDK_ALLOWLIST_REJECT_AFTER")) { + events = [ + ...buildResponsesEvents("already streamed", id).slice(0, 4), + { + ...rejection, + error: { + ...rejection.error, + code: "invalid_request_body", + message: "allowed_tools rejected", + }, + }, + ]; + } else if (input.includes("SDK_REMOVAL_RESTORED")) { + events = buildResponsesEvents("SDK_REMOVAL_RESTORED_OK", id); + } else if (input.includes("SDK_ALLOWLIST_REJECT_BEFORE")) { + events = buildResponsesEvents("SDK_ALLOWLIST_RECOVERED", id); + } else if (positionedUpdate && input.includes("SDK_RESPONSES_REJECT_BEFORE")) { + events = [rejection]; + } else if (positionedUpdate && input.includes("SDK_RESPONSES_REJECT_AFTER")) { + events = [...buildResponsesEvents("already streamed", id).slice(0, 4), rejection]; + } else if (input.includes("SDK_RESPONSES_LEGACY_FOLLOW_UP")) { + events = buildResponsesEvents("SDK_RESPONSES_LEGACY_OK", id); + } else if (input.includes("SDK_RESPONSES_REJECT_BEFORE")) { + events = buildResponsesEvents("SDK_RESPONSES_RECOVERED", id); + } else if ( + input.includes("SDK_RESPONSES_AFTER") && + !input.includes("SDK_ADDED_TOOL_RESULT") + ) { + events = buildFunctionCallEvents(id); + } else { + events = buildResponsesEvents( + input.includes("SDK_ADDED_TOOL_RESULT") ? "SDK_RESPONSES_TOOL_OK" : WS_TEXT, + id + ); + } + for (const event of events) { socket.send(JSON.stringify(event)); } - void raw; }); }); @@ -124,6 +219,8 @@ async function startFakeUpstream(): Promise<{ url, server: httpServer, wsRequestCount: () => wsRequests, + wsBodies: () => [...wsBodies], + httpBodies: () => [...httpBodies], async close() { wss.clients.forEach((c) => c.terminate()); await new Promise((resolve) => wss.close(() => resolve())); @@ -132,6 +229,35 @@ async function startFakeUpstream(): Promise<{ }; } +function buildFunctionCallEvents(id: string): Array> { + const call = { + type: "function_call", + id: "fc_sdk_added", + call_id: "call_sdk_added", + name: "sdk_epoch_added", + arguments: "{}", + status: "completed", + }; + return [ + { + type: "response.created", + response: { id, object: "response", status: "in_progress", output: [] }, + }, + { type: "response.output_item.added", output_index: 0, item: call }, + { type: "response.output_item.done", output_index: 0, item: call }, + { + type: "response.completed", + response: { + id, + object: "response", + status: "completed", + output: [call], + usage: { input_tokens: 5, output_tokens: 7, total_tokens: 12 }, + }, + }, + ]; +} + function sendJson(res: ServerResponse, status: number, body: unknown): void { res.writeHead(status, { "content-type": "application/json" }); res.end(JSON.stringify(body)); @@ -347,4 +473,359 @@ describe("CopilotRequestHandler — single subclass handles HTTP + WebSocket", a // Validate the final assistant response arrived (guards against truncated captures) expect(resultJson).toMatch(/OK from synthetic (HTTP|WS) upstream/); }, 90_000); + + it("applies SDK tool additions at the WebSocket boundary without a stale continuation", async () => { + const original = { + name: "sdk_epoch_original", + description: "Original SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const added = { + name: "sdk_epoch_added", + description: "Added SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + let addedToolCalls = 0; + await client.start(); + const beforeRequests = upstream.wsBodies().length; + const session = await client.createSession({ + model: "gpt-6-sol", + onPermissionRequest: approveAll, + capi: { enableWebSocketResponses: true }, + toolSearch: { enabled: false }, + tools: [ + defineTool(original.name, { + description: original.description, + handler: () => "ORIGINAL_TOOL_RESULT", + }), + defineTool(added.name, { + description: added.description, + handler: () => { + addedToolCalls++; + return "SDK_ADDED_TOOL_RESULT"; + }, + }), + ], + }); + try { + await session.rpc.tools.initializeAndValidate(); + await session.rpc.tools.set({ tools: [original] }); + const first = await session.sendAndWait({ prompt: "SDK_RESPONSES_FIRST" }); + expect(first?.data.content).toContain(WS_TEXT); + + await session.rpc.tools.set({ tools: [original, added] }); + const second = await session.sendAndWait({ + prompt: "SDK_RESPONSES_AFTER: use sdk_epoch_added now.", + }); + // Validate the final assistant response arrived (guards against truncated captures) + expect(second?.data.content).toContain("SDK_RESPONSES_TOOL_OK"); + expect(addedToolCalls).toBe(1); + + const requests = upstream.wsBodies().slice(beforeRequests); + expect(requests).toHaveLength(3); + const [before, update, afterTool] = requests; + expect(update.tools).toEqual(before.tools); + expect(update.previous_response_id ?? null).toBeNull(); + const input = update.input as Array>; + expect(input).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "additional_tools", + tools: expect.arrayContaining([ + expect.objectContaining({ name: added.name }), + ]), + }), + ]) + ); + const additionIndex = input.findIndex((item) => item.type === "additional_tools"); + expect(additionIndex).toBeGreaterThan(0); + expect(JSON.stringify(input.slice(0, additionIndex))).toContain("SDK_RESPONSES_AFTER"); + expect(JSON.stringify(input.slice(additionIndex + 1))).not.toContain( + "SDK_RESPONSES_AFTER" + ); + expect(JSON.stringify(afterTool.input)).toContain("SDK_ADDED_TOOL_RESULT"); + } finally { + await session.disconnect(); + } + }, 90_000); + + it("keeps the SDK removal prefix while restricting tools across continuations and restoration", async () => { + const original = { + name: "sdk_epoch_original", + description: "Original SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const added = { + ...original, + name: "sdk_epoch_added", + description: "Added SDK tool", + } as const; + let survivorCalls = 0; + await client.start(); + const beforeRequests = upstream.wsBodies().length; + const session = await client.createSession({ + model: "gpt-6-sol", + onPermissionRequest: approveAll, + capi: { enableWebSocketResponses: true }, + toolSearch: { enabled: false }, + availableTools: [original.name, added.name], + tools: [ + defineTool(original.name, { + description: original.description, + handler: () => { + throw new Error("Removed SDK tool was executed"); + }, + }), + defineTool(added.name, { + description: added.description, + handler: () => { + survivorCalls++; + return "SDK_ADDED_TOOL_RESULT"; + }, + }), + ], + }); + try { + await session.rpc.tools.initializeAndValidate(); + await session.rpc.tools.set({ tools: [original, added] }); + expect( + (await session.sendAndWait({ prompt: "SDK_REMOVAL_FIRST" }))?.data.content + ).toContain(WS_TEXT); + await session.rpc.tools.set({ tools: [added] }); + const used = await session.sendAndWait({ + prompt: "SDK_RESPONSES_AFTER: use sdk_epoch_added now.", + }); + expect(used?.data.content).toContain("SDK_RESPONSES_TOOL_OK"); + expect(survivorCalls).toBe(1); + let requests = upstream.wsBodies().slice(beforeRequests); + expect(requests).toHaveLength(3); + const [before, removed, afterTool] = requests; + const choice = { + type: "allowed_tools", + mode: "auto", + tools: [{ type: "function", name: added.name }], + }; + expect(removed.tools).toEqual(before.tools); + expect(removed.instructions).toEqual(before.instructions); + expect(removed.tool_choice).toEqual(choice); + expect(afterTool.tool_choice).toEqual(choice); + expect(removed.previous_response_id ?? null).toBeNull(); + expect(JSON.stringify(removed.input)).toContain("SDK_REMOVAL_FIRST"); + + await session.rpc.tools.set({ tools: [] }); + expect( + (await session.sendAndWait({ prompt: "SDK_REMOVAL_ALL" }))?.data.content + ).toContain(HTTP_TEXT); + // No remaining tools selects the runtime's single-shot HTTP route. + const allRemoved = upstream + .httpBodies() + .findLast((body) => JSON.stringify(body.input).includes("SDK_REMOVAL_ALL"))!; + expect(allRemoved.tools).toEqual([]); + expect(allRemoved.tool_choice).toBeUndefined(); + expect(allRemoved.previous_response_id ?? null).toBeNull(); + + await session.rpc.tools.set({ tools: [original, added] }); + const restored = await session.sendAndWait({ prompt: "SDK_REMOVAL_RESTORED" }); + // Validate the final assistant response arrived (guards against truncated captures) + expect(restored?.data.content).toContain("SDK_REMOVAL_RESTORED_OK"); + requests = upstream.wsBodies().slice(beforeRequests); + const last = requests.at(-1)!; + expect(last.tools).toEqual(before.tools); + expect(last.tool_choice).not.toEqual( + expect.objectContaining({ type: "allowed_tools" }) + ); + expect(last.previous_response_id ?? null).toBeNull(); + expect(JSON.stringify(last.input)).not.toContain('"type":"additional_tools"'); + } finally { + await session.disconnect(); + } + }, 90_000); + + for (const phase of ["BEFORE", "AFTER"] as const) { + it(`only replays a rejected SDK tool allowlist ${phase.toLowerCase()} output`, async () => { + const original = { + name: "sdk_epoch_original", + description: "Original SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const added = { + ...original, + name: "sdk_epoch_added", + description: "Added SDK tool", + } as const; + await client.start(); + const beforeRequests = upstream.wsBodies().length; + const session = await client.createSession({ + model: "gpt-6-sol", + onPermissionRequest: approveAll, + capi: { enableWebSocketResponses: true }, + toolSearch: { enabled: false }, + availableTools: [original.name, added.name], + tools: [original, added].map((tool) => + defineTool(tool.name, { + description: tool.description, + handler: () => "UNUSED", + }) + ), + }); + let failedTurnIdle = false; + const unsubscribe = session.on((event) => { + if (event.type === "session.idle" && !event.agentId) { + failedTurnIdle = true; + } + }); + try { + await session.rpc.tools.initializeAndValidate(); + await session.rpc.tools.set({ tools: [original, added] }); + expect( + (await session.sendAndWait({ prompt: "SDK_ALLOWLIST_FIRST" }))?.data.content + ).toContain(WS_TEXT); + failedTurnIdle = false; + await session.rpc.tools.set({ tools: [added] }); + const changed = session.sendAndWait({ prompt: `SDK_ALLOWLIST_REJECT_${phase}` }); + if (phase === "BEFORE") { + expect((await changed)?.data.content).toContain("SDK_ALLOWLIST_RECOVERED"); + } else { + await expect(changed).rejects.toThrow(/allowed_tools rejected/); + await waitForCondition(() => failedTurnIdle, { + timeoutMessage: "Rejected SDK allowlist did not reach session.idle", + }); + } + const requests = upstream.wsBodies().slice(beforeRequests); + expect(requests).toHaveLength(phase === "BEFORE" ? 3 : 2); + expect(requests[1].tools).toEqual(requests[0].tools); + expect(requests[1].tool_choice).toMatchObject({ type: "allowed_tools" }); + expect(requests[1].previous_response_id ?? null).toBeNull(); + if (phase === "BEFORE") { + expect(requests[2].tool_choice).not.toEqual( + expect.objectContaining({ type: "allowed_tools" }) + ); + expect(JSON.stringify(requests[2].tools)).not.toContain(original.name); + expect(requests[2].previous_response_id ?? null).toBeNull(); + } + const followUp = await session.sendAndWait({ + prompt: "SDK_RESPONSES_LEGACY_FOLLOW_UP", + }); + // Validate the final assistant response arrived (guards against truncated captures) + expect(followUp?.data.content).toContain("SDK_RESPONSES_LEGACY_OK"); + const last = upstream.wsBodies().at(-1)!; + expect(last.tool_choice).not.toEqual( + expect.objectContaining({ type: "allowed_tools" }) + ); + expect(JSON.stringify(last.tools)).not.toContain(original.name); + } finally { + unsubscribe(); + await session.disconnect(); + } + }, 90_000); + } + + for (const phase of ["BEFORE", "AFTER"] as const) { + it(`only replays a rejected SDK WebSocket tool update ${phase.toLowerCase()} output`, async () => { + const original = { + name: "sdk_epoch_original", + description: "Original SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const added = { + name: "sdk_epoch_added", + description: "Added SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + await client.start(); + const beforeRequests = upstream.wsBodies().length; + const session = await client.createSession({ + model: "gpt-6-sol", + onPermissionRequest: approveAll, + capi: { enableWebSocketResponses: true }, + toolSearch: { enabled: false }, + tools: [ + defineTool(original.name, { + description: original.description, + handler: () => "ORIGINAL_TOOL_RESULT", + }), + defineTool(added.name, { + description: added.description, + handler: () => "SDK_ADDED_TOOL_RESULT", + }), + ], + }); + let failedTurnIdle = false; + const unsubscribe = session.on((event) => { + if (event.type === "session.idle" && !event.agentId) { + failedTurnIdle = true; + } + }); + try { + await session.rpc.tools.initializeAndValidate(); + await session.rpc.tools.set({ tools: [original] }); + const first = await session.sendAndWait({ prompt: "SDK_RESPONSES_FIRST" }); + expect(first?.data.content).toContain(WS_TEXT); + failedTurnIdle = false; + + await session.rpc.tools.set({ tools: [original, added] }); + const changedTurn = session.sendAndWait({ + prompt: `SDK_RESPONSES_REJECT_${phase}: use sdk_epoch_added now.`, + }); + if (phase === "BEFORE") { + const recovered = await changedTurn; + expect(recovered?.data.content).toContain("SDK_RESPONSES_RECOVERED"); + } else { + await expect(changedTurn).rejects.toThrow(/additional_tools rejected/); + await waitForCondition(() => failedTurnIdle, { + timeoutMessage: "Rejected SDK tool update did not reach session.idle", + }); + } + + const requests = upstream.wsBodies().slice(beforeRequests); + expect(requests).toHaveLength(phase === "BEFORE" ? 3 : 2); + const [before, rejected, retry] = requests; + expect(rejected.tools).toEqual(before.tools); + expect(rejected.previous_response_id ?? null).toBeNull(); + const positioned = (rejected.input as Array<{ type?: string }>).filter( + (item) => item.type === "additional_tools" + ); + expect(positioned).toHaveLength(1); + expect(JSON.stringify(positioned[0])).toContain(added.name); + if (phase === "BEFORE") { + expect(retry?.previous_response_id ?? null).toBeNull(); + expect(JSON.stringify(retry?.input)).not.toContain('"type":"additional_tools"'); + expect(JSON.stringify(retry?.tools)).toContain(added.name); + + const followUp = await session.sendAndWait({ + prompt: "SDK_RESPONSES_LEGACY_FOLLOW_UP", + }); + expect(followUp?.data.content).toContain("SDK_RESPONSES_LEGACY_OK"); + const followUpRequest = upstream.wsBodies().at(-1); + expect(JSON.stringify(followUpRequest?.tools)).toEqual( + JSON.stringify(retry?.tools) + ); + expect(JSON.stringify(followUpRequest?.input)).not.toContain( + '"type":"additional_tools"' + ); + } else { + const followUp = await session.sendAndWait({ + prompt: "SDK_RESPONSES_LEGACY_FOLLOW_UP", + }); + expect(followUp?.data.content).toContain("SDK_RESPONSES_LEGACY_OK"); + const followUpRequest = upstream.wsBodies().at(-1); + expect(upstream.wsBodies().slice(beforeRequests)).toHaveLength(3); + expect(followUpRequest?.previous_response_id ?? null).toBeNull(); + expect(JSON.stringify(followUpRequest?.tools)).toContain(added.name); + expect(JSON.stringify(followUpRequest?.input)).not.toContain( + '"type":"additional_tools"' + ); + } + } finally { + unsubscribe(); + await session.disconnect(); + } + }, 90_000); + } }); diff --git a/nodejs/test/e2e/customizations_reload.e2e.test.ts b/nodejs/test/e2e/customizations_reload.e2e.test.ts new file mode 100644 index 0000000000..2f33e04791 --- /dev/null +++ b/nodejs/test/e2e/customizations_reload.e2e.test.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { execFileSync } from "node:child_process"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { approveAll } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +describe("Session customization reload", async () => { + const { copilotClient: client, workDir } = await createSdkTestContext(); + + it("discovers repository instructions created after session startup without changing its cwd", async () => { + execFileSync("git", ["init", "--quiet"], { cwd: workDir }); + const session = await client.createSession({ onPermissionRequest: approveAll }); + + try { + const before = await session.rpc.metadata.snapshot(); + const marker = "SDK_RELOAD_NEW_REPOSITORY_INSTRUCTIONS"; + expect( + (await session.rpc.instructions.getSources()).sources.some((source) => + source.content.includes(marker) + ) + ).toBe(false); + + const instructionsDir = join(workDir, ".github"); + mkdirSync(instructionsDir); + writeFileSync( + join(instructionsDir, "copilot-instructions.md"), + `Repository instructions: ${marker}\n` + ); + + const reload = await session.rpc.customizations.reload(); + expect(reload.errors).toEqual([]); + expect(reload.outcomes).toEqual( + expect.arrayContaining([ + expect.objectContaining({ subsystem: "repositoryContext", status: "reloaded" }), + expect.objectContaining({ subsystem: "instructions", status: "reloaded" }), + ]) + ); + expect( + (await session.rpc.instructions.getSources()).sources.some( + (source) => + source.type === "repo" && + source.location === "repository" && + source.content.includes(marker) + ) + ).toBe(true); + expect((await session.rpc.metadata.snapshot()).workingDirectory).toBe( + before.workingDirectory + ); + } finally { + await session.disconnect(); + } + }); +}); diff --git a/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts b/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts index 8478f93288..f771e54dee 100644 --- a/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts +++ b/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts @@ -223,7 +223,10 @@ describe("disabled MCP servers", async () => { }, }); - function createPluginDirectory(prefix: string): { + function createPluginDirectory( + prefix: string, + controlArgs: string[] = [] + ): { pluginDirectory: string; controlMarker: string; disabledMarker: string; @@ -253,6 +256,7 @@ describe("disabled MCP servers", async () => { controlMarker, "--server-name", "control", + ...controlArgs, ], }, disabled: { @@ -339,6 +343,194 @@ describe("disabled MCP servers", async () => { ); } + it.each(["connected", "failed"] as const)( + "lists cold configured servers without starting or awaiting them before a %s first-turn startup", + { timeout: 120_000 }, + async (expectedStatus) => { + const startupGate = join(workDir, `startup-gate-${randomUUID()}`); + const { pluginDirectory, controlMarker, disabledMarker } = createPluginDirectory( + "configured-mcp", + [ + "--startup-gate", + startupGate, + ...(expectedStatus === "failed" ? ["--fail-startup"] : []), + ] + ); + await using session = await client.createSession({ + onPermissionRequest: approveAll, + pluginDirectories: [pluginDirectory], + disabledMcpServers: ["disabled"], + }); + + for (let i = 0; i < 2; i++) { + const inventory = await session.rpc.mcp.listConfigured(); + expect( + inventory.servers + .filter((server) => ["control", "disabled"].includes(server.name)) + .map(({ name, enabled, live }) => ({ name, enabled, live })) + .sort((a, b) => a.name.localeCompare(b.name)) + ).toEqual([ + { name: "control", enabled: true, live: undefined }, + { name: "disabled", enabled: false, live: undefined }, + ]); + expect(markerCount(controlMarker)).toBe(0); + expect(markerCount(disabledMarker)).toBe(0); + } + + const [response] = await Promise.all([ + session.sendAndWait({ prompt: MCP_TRIGGER_PROMPT }, 90_000), + (async () => { + try { + await waitForMarkerCount(controlMarker, 1); + // Inventory must also return while initialize is held behind the gate. + const inventory = await session.rpc.mcp.listConfigured(); + expect( + inventory.servers.find((server) => server.name === "control")?.live + ?.status + ).toBe("pending"); + expect(markerCount(controlMarker)).toBe(1); + } finally { + writeFileSync(startupGate, "release"); + } + })(), + ]); + expectSyntheticResponse(response); + const startsAfterTurn = markerCount(controlMarker); + if (expectedStatus === "connected") { + expect(startsAfterTurn).toBe(1); + } + // Turn initialization may retry a failed server; inventory must not. + for (let i = 0; i < 2; i++) { + const inventory = await session.rpc.mcp.listConfigured(); + expect( + inventory.servers.find((server) => server.name === "control")?.live?.status + ).toBe(expectedStatus); + expect(markerCount(controlMarker)).toBe(startsAfterTurn); + } + expect(markerCount(disabledMarker)).toBe(0); + } + ); + + // SDK runners provide a sandbox backend only on macOS (no bwrap/BaseContainer elsewhere). + it.skipIf(process.platform !== "darwin").each([ + ["create", "connected"], + ["create", "failed"], + ["resume", "connected"], + ["resume", "failed"], + ] as const)( + "%s applies sandbox without starting MCP and skips a gated %s startup", + { timeout: 120_000 }, + async (mode, expectedStatus) => { + const startupGate = join(workDir, `sandbox-startup-gate-${randomUUID()}`); + const { pluginDirectory, controlMarker, disabledMarker } = createPluginDirectory( + "sandbox-mcp", + [ + "--startup-gate", + startupGate, + ...(expectedStatus === "failed" ? ["--fail-startup"] : []), + ] + ); + const createOptions = { + onPermissionRequest: approveAll, + enableSessionStore: true, + pluginDirectories: [pluginDirectory], + disabledMcpServers: ["disabled"], + }; + const activeClient = createClient({ + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + requestHandler: new PersistingRequestHandler(), + }); + let sessionId: string | undefined; + if (mode === "resume") { + const seedClient = createClient({ + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + requestHandler: new PersistingRequestHandler(), + }); + try { + const seed = await seedClient.createSession({ + onPermissionRequest: approveAll, + enableSessionStore: true, + }); + sessionId = seed.sessionId; + expectSyntheticResponse(await seed.sendAndWait({ prompt: MCP_TRIGGER_PROMPT })); + } finally { + await seedClient.stop(); + } + } + try { + await using session = sessionId + ? await activeClient.resumeSession(sessionId, createOptions) + : await activeClient.createSession(createOptions); + let startupFailure: string | undefined; + const unsubscribe = session.on("session.mcp_server_status_changed", (event) => { + if (event.data.serverName === "control" && event.data.status === "failed") { + startupFailure = event.data.error ?? "MCP startup failed"; + } + }); + try { + const updated = await session.rpc.options.update({ + sandboxConfig: { + enabled: true, + allowBypass: true, + addCurrentWorkingDirectory: true, + allowDevToolAccess: true, + userPolicy: { + // The fixture and its dependencies live outside the session workspace. + filesystem: { + readonlyPaths: [resolve(__dirname, "../../../../..")], + }, + }, + }, + }); + expect(updated.success).toBe(true); + const inventory = await session.rpc.mcp.listConfigured(); + expect(inventory.servers.filter((s) => s.name === "control")).toMatchObject([ + { enabled: true }, + ]); + expect(markerCount(controlMarker)).toBe(0); + expect(markerCount(disabledMarker)).toBe(0); + const [response] = await Promise.all([ + session.sendAndWait({ prompt: MCP_TRIGGER_PROMPT }, 60_000), + (async () => { + await waitForCondition( + () => { + if (startupFailure) { + throw new Error(startupFailure); + } + return markerCount(controlMarker) === 1; + }, + { timeoutMs: 60_000, intervalMs: 100 } + ); + const background = await session.rpc.mcp.moveLoadingToBackground(); + expect(background.movedToBackground).toBe(true); + })(), + ]); + expectSyntheticResponse(response); + expect(existsSync(startupGate)).toBe(false); + expect(markerCount(controlMarker)).toBe(1); + expect(markerCount(disabledMarker)).toBe(0); + writeFileSync(startupGate, "release"); + await waitForCondition( + async () => { + const configured = await session.rpc.mcp.listConfigured(); + return ( + configured.servers.find((server) => server.name === "control")?.live + ?.status === expectedStatus + ); + }, + { timeoutMs: 60_000, intervalMs: 100 } + ); + expect(markerCount(controlMarker)).toBe(1); + } finally { + unsubscribe(); + writeFileSync(startupGate, "release"); + } + } finally { + await activeClient.stop(); + } + } + ); + it( "keeps disabled plugin MCP servers per-session on create", { timeout: 120_000 }, @@ -395,6 +587,8 @@ describe("disabled MCP servers", async () => { const disabledSession = await client.createSession({ onPermissionRequest: approveAll, enableConfigDiscovery: true, + // This exercises MCP discovery, not standalone extension hosting. + requestExtensions: false, enableMcpApps: true, githubMcpToolConfig: { enableAllTools: true }, disabledMcpServers: ["github-mcp-server"], @@ -420,6 +614,7 @@ describe("disabled MCP servers", async () => { await using enabledSession = await client.createSession({ onPermissionRequest: approveAll, enableConfigDiscovery: true, + requestExtensions: false, enableMcpApps: true, githubMcpToolConfig: { enableAllTools: true }, }); diff --git a/nodejs/test/e2e/extension_env_access.e2e.test.ts b/nodejs/test/e2e/extension_env_access.e2e.test.ts index 15da3a9327..236201a21b 100644 --- a/nodejs/test/e2e/extension_env_access.e2e.test.ts +++ b/nodejs/test/e2e/extension_env_access.e2e.test.ts @@ -103,8 +103,9 @@ async function runExtensionAgainstStubHost(options: { await retry( "wait for the fixture extension to report its join result", async () => { + // File creation is visible before the child writes the result. expect( - existsSync(resultFile), + existsSync(resultFile) && readFileSync(resultFile, "utf-8") !== "", `extension never reported; stderr: ${stderr.join("")}` ).toBe(true); }, diff --git a/nodejs/test/e2e/github_telemetry.e2e.test.ts b/nodejs/test/e2e/github_telemetry.e2e.test.ts index e33178f9d0..ad0c5f398f 100644 --- a/nodejs/test/e2e/github_telemetry.e2e.test.ts +++ b/nodejs/test/e2e/github_telemetry.e2e.test.ts @@ -54,4 +54,65 @@ describe("GitHub telemetry forwarding", async () => { await session.disconnect(); } ); + + it("keeps telemetry ownership across disconnect and same-id recreation", async () => { + const session = await client.createSession({ onPermissionRequest: approveAll }); + const firstIdentity = await session.rpc.telemetry.getEngagementId(); + try { + await session.setModel("gpt-4.1"); + await waitForCondition( + () => + received.some( + (notification) => + notification.sessionId === session.sessionId && + notification.event.kind === "session_model_change" && + notification.event.properties?.engagement_id === + firstIdentity.engagementId + ), + { timeoutMessage: "The original pipeline did not forward its model change." } + ); + } finally { + await session.disconnect(); + } + + // Native race tests control delayed enrichment. This boundary test proves + // the close/recreate path and forwarding transport retain the same ownership. + const resumeOffset = received.length; + const resumed = await client.createSession({ + sessionId: session.sessionId, + model: "gpt-4.1", + onPermissionRequest: approveAll, + }); + try { + const secondIdentity = await resumed.rpc.telemetry.getEngagementId(); + expect(firstIdentity.engagementId).toBeTruthy(); + expect(secondIdentity.engagementId).toBeTruthy(); + expect(secondIdentity.engagementId).not.toBe(firstIdentity.engagementId); + await resumed.setModel("gpt-5.4"); + await waitForCondition( + () => + received + .slice(resumeOffset) + .some( + (notification) => + notification.sessionId === resumed.sessionId && + notification.event.kind === "session_model_change" && + notification.event.properties?.engagement_id === + secondIdentity.engagementId + ), + { timeoutMessage: "The replacement pipeline did not forward its model change." } + ); + const afterResume = received + .slice(resumeOffset) + .filter((notification) => notification.sessionId === resumed.sessionId); + expect( + afterResume.some( + (notification) => + notification.event.properties?.engagement_id === firstIdentity.engagementId + ) + ).toBe(false); + } finally { + await resumed.disconnect(); + } + }); }); diff --git a/nodejs/test/e2e/harness/CapiProxy.test.ts b/nodejs/test/e2e/harness/CapiProxy.test.ts new file mode 100644 index 0000000000..1c804d7e50 --- /dev/null +++ b/nodejs/test/e2e/harness/CapiProxy.test.ts @@ -0,0 +1,342 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { spawn } from "child_process"; +import { once } from "node:events"; +import { existsSync } from "node:fs"; +import { copyFile, mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { createInterface } from "node:readline"; +import { beforeEach, describe, expect, inject, it, onTestFinished, vi } from "vitest"; +import { CapiProxy } from "./CapiProxy"; +import { CAPI_PROXY_BUNDLE } from "./proxyBundleContext"; +import { hasChildExited, stopChildProcess, waitForChildExit } from "./sdkTestHelper"; + +vi.mock("child_process", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, spawn: vi.fn(actual.spawn) }; +}); + +vi.mock("./sdkTestHelper", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + stopChildProcess: vi.fn(actual.stopChildProcess), + waitForChildExit: vi.fn(actual.waitForChildExit), + }; +}); + +const realSpawn = vi.mocked(spawn).getMockImplementation()!; +const realWaitForChildExit = vi.mocked(waitForChildExit).getMockImplementation()!; + +async function startOwnedProxy(): Promise { + const proxy = new CapiProxy(); + const starting = proxy.start(); + onTestFinished(async () => { + await Promise.allSettled([starting]); + await proxy.stop(true); + }); + await starting; + return proxy; +} + +describe("bundled CAPI proxy", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("launches the shared bundle directly and waits for a clean shutdown", async () => { + const proxy = await startOwnedProxy(); + const serverPath = inject(CAPI_PROXY_BUNDLE); + expect(serverPath).toMatch(/server\.mjs$/); + expect(existsSync(serverPath)).toBe(true); + expect(spawn).toHaveBeenCalledExactlyOnceWith(process.execPath, [serverPath], { + stdio: ["ignore", "pipe", "inherit"], + windowsHide: true, + }); + const child = vi.mocked(spawn).mock.results[0].value; + expect(child.exitCode).toBeNull(); + expect(proxy.getProxyEnv()).toMatchObject({ + HTTPS_PROXY: expect.stringMatching(/^http:\/\/127\.0\.0\.1:/), + NODE_EXTRA_CA_CERTS: expect.any(String), + }); + expect(existsSync(proxy.getProxyEnv().NODE_EXTRA_CA_CERTS)).toBe(true); + + const proxyUrl = proxy.url; + await proxy.stop(true); + expect(child.exitCode).toBe(0); + expect(child.signalCode).toBeNull(); + expect(() => proxy.url).toThrow("has not been started"); + await expect(fetch(proxyUrl)).rejects.toThrow(); + }); + + it("shares only the bundle, not authentication state, ports, or certificates", async () => { + const [first, second] = await Promise.all([startOwnedProxy(), startOwnedProxy()]); + expect(first.url).not.toBe(second.url); + expect(first.getProxyEnv().HTTPS_PROXY).not.toBe(second.getProxyEnv().HTTPS_PROXY); + expect(first.getProxyEnv().NODE_EXTRA_CA_CERTS).not.toBe( + second.getProxyEnv().NODE_EXTRA_CA_CERTS + ); + expect(vi.mocked(spawn).mock.calls.map((call) => call[1])).toEqual([ + [inject(CAPI_PROXY_BUNDLE)], + [inject(CAPI_PROXY_BUNDLE)], + ]); + + await first.setCopilotUserByToken("isolated-token", { login: "first-user" }); + await second.setCopilotUserByToken("isolated-token", { login: "second-user" }); + for (const [proxy, login] of [ + [first, "first-user"], + [second, "second-user"], + ] as const) { + const response = await fetch(`${proxy.url}/copilot_internal/user`, { + headers: { Authorization: "Bearer isolated-token" }, + }); + expect(response.ok).toBe(true); + expect(await response.json()).toMatchObject({ login }); + } + }); + + it("runs the self-contained bundle from a path containing spaces", async () => { + const directory = await mkdtemp(join(tmpdir(), "sdk proxy bundle ")); + const proxy = new CapiProxy(); + let starting: Promise | undefined; + onTestFinished(async () => { + await Promise.allSettled(starting ? [starting] : []); + try { + await proxy.stop(true); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }); + const serverPath = join(directory, "server with spaces.mjs"); + await copyFile(inject(CAPI_PROXY_BUNDLE), serverPath); + vi.mocked(spawn).mockImplementationOnce((command, _args, options) => + realSpawn(command, [serverPath], options) + ); + starting = proxy.start(); + await starting; + await proxy.setCopilotUserByToken("spaces-token", { login: "spaces-user" }); + const response = await fetch(`${proxy.url}/copilot_internal/user`, { + headers: { Authorization: "Bearer spaces-token" }, + }); + expect(await response.json()).toMatchObject({ login: "spaces-user" }); + }); + + it("reports early startup exit and its output", async () => { + vi.mocked(spawn).mockImplementationOnce((_command, _args, options) => + realSpawn( + process.execPath, + ["-e", 'console.log("startup failed"); process.exit(23);'], + options + ) + ); + await expect(startOwnedProxy()).rejects.toThrow( + "Proxy exited before startup with code 23: startup failed" + ); + expect(vi.mocked(spawn).mock.results[0].value.exitCode).toBe(23); + }); + + it("terminates the owned child when startup metadata is invalid", async () => { + vi.mocked(spawn).mockImplementationOnce((_command, _args, options) => + realSpawn( + process.execPath, + [ + "-e", + 'console.log("Listening: http://127.0.0.1:1 {}"); setInterval(() => {}, 1000);', + ], + options + ) + ); + await expect(startOwnedProxy()).rejects.toThrow("missing CONNECT proxy details"); + const child = vi.mocked(spawn).mock.results[0].value; + expect(child.exitCode !== null || child.signalCode !== null).toBe(true); + }); + + it("reports spawn failures without waiting for an impossible exit", async () => { + vi.mocked(spawn).mockImplementationOnce(() => + realSpawn(join(dirname(inject(CAPI_PROXY_BUNDLE)), "missing-node-executable"), [], {}) + ); + await expect(startOwnedProxy()).rejects.toMatchObject({ code: "ENOENT" }); + }); + + it("retains a child after startup cleanup fails so shutdown can retry", async () => { + const cleanupError = new Error("Child process did not exit after SIGKILL"); + vi.mocked(stopChildProcess).mockRejectedValueOnce(cleanupError); + vi.mocked(spawn).mockImplementationOnce((_command, _args, options) => + realSpawn( + process.execPath, + [ + "-e", + 'console.log("Listening: http://127.0.0.1:1 {}"); setInterval(() => {}, 1000);', + ], + options + ) + ); + const proxy = new CapiProxy(); + const starting = proxy.start(); + const child = vi.mocked(spawn).mock.results[0].value; + onTestFinished(async () => { + await Promise.allSettled([starting]); + try { + await proxy.stop(true); + } finally { + await stopChildProcess(child); + } + }); + + await expect(starting).rejects.toMatchObject({ + message: "Proxy startup and cleanup failed", + errors: [ + expect.objectContaining({ + message: expect.stringContaining("missing CONNECT proxy details"), + }), + cleanupError, + ], + }); + expect(hasChildExited(child)).toBe(false); + await expect(proxy.start()).rejects.toThrow("already been started"); + expect(spawn).toHaveBeenCalledTimes(1); + + await proxy.stop(true); + expect(hasChildExited(child)).toBe(true); + }); + + it("retains a running proxy after shutdown cleanup fails so shutdown can retry", async () => { + const proxy = new CapiProxy(); + const starting = proxy.start(); + const child = vi.mocked(spawn).mock.results[0].value; + const fetchMock = vi.spyOn(globalThis, "fetch"); + onTestFinished(async () => { + fetchMock.mockRestore(); + await Promise.allSettled([starting]); + try { + await proxy.stop(true); + } finally { + await stopChildProcess(child); + } + }); + const proxyUrl = await starting; + const proxyEnv = proxy.getProxyEnv(); + const shutdownError = new Error("Shutdown request failed"); + const cleanupError = new Error("Child process did not exit after SIGKILL"); + fetchMock.mockRejectedValueOnce(shutdownError); + vi.mocked(stopChildProcess).mockRejectedValueOnce(cleanupError); + + await expect(proxy.stop(true)).rejects.toMatchObject({ + message: "Proxy shutdown and cleanup failed", + errors: [shutdownError, cleanupError], + }); + expect(hasChildExited(child)).toBe(false); + expect(proxy.url).toBe(proxyUrl); + expect(proxy.getProxyEnv()).toEqual(proxyEnv); + await expect(proxy.start()).rejects.toThrow("already been started"); + expect(spawn).toHaveBeenCalledTimes(1); + + await proxy.stop(true); + expect(child.exitCode).toBe(0); + expect(child.signalCode).toBeNull(); + expect(() => proxy.url).toThrow("has not been started"); + expect(proxy.getProxyEnv()).toEqual({}); + await expect(fetch(proxyUrl)).rejects.toThrow(); + }); + + it("allows an acknowledged capture flush to finish beyond the old shutdown cutoff", async () => { + const directory = await mkdtemp(join(tmpdir(), "sdk proxy flush ")); + const capturePath = join(directory, "capture.yaml"); + vi.mocked(spawn).mockImplementationOnce((_command, _args, options) => + realSpawn( + process.execPath, + [ + "-e", + ` +const { createServer } = require("node:http"); +const { writeFile } = require("node:fs/promises"); +let releaseFlush; +const flushGate = new Promise(resolve => { releaseFlush = resolve; }); +const server = createServer(async (request, response) => { + if (request.url === "/stop") { + response.end(); + await writeFile(process.argv[1], "partial capture"); + console.log("Capture flush blocked"); + await flushGate; + await writeFile(process.argv[1], "complete capture"); + server.close(); + process.exit(0); + } else if (request.url === "/release") { + response.end(); + releaseFlush(); + } +}); +server.listen(0, "127.0.0.1", () => { + const url = "http://127.0.0.1:" + server.address().port; + console.log("Listening: " + url + " " + JSON.stringify({ + connectProxyUrl: url, + caFilePath: "unused.pem" + })); +}); +`, + capturePath, + ], + options + ) + ); + const proxy = new CapiProxy(); + const starting = proxy.start(); + const child = vi.mocked(spawn).mock.results[0].value; + let outputLines: ReturnType | undefined; + let stopping: Promise | undefined; + onTestFinished(async () => { + vi.useRealTimers(); + outputLines?.close(); + try { + await stopChildProcess(child); + await Promise.allSettled([starting, ...(stopping ? [stopping] : [])]); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }); + const proxyUrl = await starting; + // Startup closes its reader and pauses stdout; attach the flush reader afterward. + outputLines = createInterface({ input: child.stdout! }); + const flushBlocked = once(outputLines, "line"); + let resolveExitWait!: () => void; + const exitWaitStarted = new Promise((resolve) => { + resolveExitWait = resolve; + }); + vi.mocked(waitForChildExit).mockImplementationOnce((process, timeoutMs) => { + vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }); + const waiting = realWaitForChildExit(process, timeoutMs); + resolveExitWait(); + return waiting; + }); + const kill = vi.spyOn(child, "kill"); + let shutdownSettled = false; + stopping = proxy.stop().then( + () => { + shutdownSettled = true; + }, + (error: unknown) => { + shutdownSettled = true; + return error; + } + ); + await exitWaitStarted; + const [output] = await flushBlocked; + expect(String(output)).toContain("Capture flush blocked"); + expect(await readFile(capturePath, "utf8")).toBe("partial capture"); + + await vi.advanceTimersByTimeAsync(10_000); + expect(shutdownSettled).toBe(false); + expect(kill).not.toHaveBeenCalled(); + expect(hasChildExited(child)).toBe(false); + + vi.useRealTimers(); + expect((await fetch(`${proxyUrl}/release`)).ok).toBe(true); + expect(await stopping).toBeUndefined(); + expect(await readFile(capturePath, "utf8")).toBe("complete capture"); + expect(child.exitCode).toBe(0); + expect(child.signalCode).toBeNull(); + }); +}); diff --git a/nodejs/test/e2e/harness/CapiProxy.ts b/nodejs/test/e2e/harness/CapiProxy.ts index c25d422f99..a59591b75f 100644 --- a/nodejs/test/e2e/harness/CapiProxy.ts +++ b/nodejs/test/e2e/harness/CapiProxy.ts @@ -1,15 +1,21 @@ -import { spawn } from "child_process"; -import { resolve } from "path"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { spawn, type ChildProcess } from "child_process"; import { createInterface } from "readline"; -import { expect } from "vitest"; -import type { CapturedRequest } from "../../../../test/harness/replayingCapiProxy"; -import { +import { expect, inject } from "vitest"; +import type { CapturedRequest, ReplayBackend } from "../../../../test/harness/replayingCapiProxy"; +import type { CopilotUserResponse, + MemoryApiStub, ParsedHttpExchange, } from "../../../../test/harness/replayingCapiProxy"; import { isCI } from "./sdkTestContext"; +import { CAPI_PROXY_BUNDLE } from "./proxyBundleContext"; +import { hasChildExited, stopChildProcess, waitForChildExit } from "./sdkTestHelper"; +import { testBackend } from "./testBackend"; -const HARNESS_SERVER_PATH = resolve(__dirname, "../../../../test/harness/server.ts"); const NO_PROXY = "127.0.0.1,localhost,::1"; interface ProxyStartupInfo { @@ -22,6 +28,7 @@ interface ProxyStartupInfo { export class CapiProxy { private proxyUrl: string | undefined; private startupInfo: ProxyStartupInfo | undefined; + private serverProcess: ChildProcess | undefined; /** * Returns the URL of the running proxy. Throws if the proxy has not been started. @@ -34,43 +41,77 @@ export class CapiProxy { } async start(): Promise { - const serverProcess = spawn("npx", ["tsx", HARNESS_SERVER_PATH], { + if (this.serverProcess) { + throw new Error("CapiProxy has already been started."); + } + const serverPath = inject(CAPI_PROXY_BUNDLE); + if (!serverPath) { + throw new Error("CapiProxy bundle is missing; enable the SDK Vitest global setup."); + } + const serverProcess = spawn(process.execPath, [serverPath], { stdio: ["ignore", "pipe", "inherit"], - shell: true, + windowsHide: true, }); + this.serverProcess = serverProcess; - this.startupInfo = await new Promise((resolve, reject) => { - const stdout = serverProcess.stdout!; - const lines: string[] = []; - const lineReader = createInterface({ input: stdout }); - const cleanup = () => { - lineReader.off("line", onLine); - serverProcess.off("exit", onExit); - lineReader.close(); - }; - const onLine = (line: string) => { - lines.push(line); - try { - const info = tryParseStartupInfo(line); - if (!info) { - return; + try { + this.startupInfo = await new Promise((resolve, reject) => { + const stdout = serverProcess.stdout!; + const lines: string[] = []; + const lineReader = createInterface({ input: stdout }); + const cleanup = () => { + lineReader.off("line", onLine); + serverProcess.off("exit", onExit); + serverProcess.off("error", onError); + lineReader.close(); + // Closing readline pauses stdout even when another reader still owns it. + stdout.resume(); + }; + const onLine = (line: string) => { + lines.push(line); + try { + const info = tryParseStartupInfo(line); + if (!info) { + return; + } + cleanup(); + resolve(info); + } catch (error) { + cleanup(); + reject(error); } + }; + const onExit = (code: number | null) => { cleanup(); - resolve(info); - } catch (error) { + reject( + new Error( + `Proxy exited before startup with code ${code}: ${lines.join("\n")}` + ) + ); + }; + const onError = (error: Error) => { cleanup(); reject(error); + }; + lineReader.on("line", onLine); + serverProcess.once("exit", onExit); + serverProcess.once("error", onError); + }); + } catch (error) { + try { + // A failed spawn has no PID and cannot emit an exit event. + if (serverProcess.pid !== undefined) { + await stopChildProcess(serverProcess); } - }; - const onExit = (code: number | null) => { - cleanup(); - reject( - new Error(`Proxy exited before startup with code ${code}: ${lines.join("\n")}`) - ); - }; - lineReader.on("line", onLine); - serverProcess.once("exit", onExit); - }); + } catch (cleanupError) { + throw new AggregateError([error, cleanupError], "Proxy startup and cleanup failed"); + } finally { + if (serverProcess.pid === undefined || hasChildExited(serverProcess)) { + this.serverProcess = undefined; + } + } + throw error; + } this.proxyUrl = this.startupInfo.capiProxyUrl; return this.proxyUrl; @@ -107,32 +148,67 @@ export class CapiProxy { async updateConfig(config: { filePath: string; workDir: string; + backend?: ReplayBackend; testInfo?: { file: string; line?: number }; + modelNames?: Record; }): Promise { const response = await fetch(`${this.proxyUrl}/config`, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify(config), + body: JSON.stringify({ backend: testBackend, ...config }), }); expect(response.ok).toBe(true); } async getExchanges(): Promise { const response = await fetch(`${this.proxyUrl}/exchanges`, { method: "GET" }); - return await response.json(); + return (await response.json()) as ParsedHttpExchange[]; } async getRequests(): Promise { const response = await fetch(`${this.proxyUrl}/requests`, { method: "GET" }); - return await response.json(); + return (await response.json()) as CapturedRequest[]; } async stop(skipWritingCache?: boolean): Promise { - const url = skipWritingCache - ? `${this.proxyUrl}/stop?skipWritingCache=true` - : `${this.proxyUrl}/stop`; - const response = await fetch(url, { method: "POST" }); - expect(response.ok).toBe(true); + const serverProcess = this.serverProcess; + if (!serverProcess) { + return; + } + try { + if (!this.proxyUrl) { + await stopChildProcess(serverProcess); + return; + } + const url = skipWritingCache + ? `${this.proxyUrl}/stop?skipWritingCache=true` + : `${this.proxyUrl}/stop`; + const response = await fetch(url, { method: "POST" }); + expect(response.ok).toBe(true); + // /stop acknowledges before captures are flushed; do not interrupt that write. + await waitForChildExit(serverProcess); + if (serverProcess.exitCode !== 0 || serverProcess.signalCode !== null) { + throw new Error( + `Proxy exited with code ${serverProcess.exitCode}, signal ${serverProcess.signalCode}` + ); + } + } catch (error) { + try { + await stopChildProcess(serverProcess); + } catch (cleanupError) { + throw new AggregateError( + [error, cleanupError], + "Proxy shutdown and cleanup failed" + ); + } + throw error; + } finally { + if (serverProcess.pid === undefined || hasChildExited(serverProcess)) { + this.serverProcess = undefined; + this.proxyUrl = undefined; + this.startupInfo = undefined; + } + } } /** @@ -148,6 +224,15 @@ export class CapiProxy { }); expect(res.ok).toBe(true); } + + async setMemoryApiStub(stub: MemoryApiStub): Promise { + const response = await fetch(`${this.proxyUrl}/memory-api-config`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(stub), + }); + expect(response.ok).toBe(true); + } } function tryParseStartupInfo(line: string): ProxyStartupInfo | undefined { diff --git a/nodejs/test/e2e/harness/ahpClient.ts b/nodejs/test/e2e/harness/ahpClient.ts index 8ad23d6e1a..13fea3ef5a 100644 --- a/nodejs/test/e2e/harness/ahpClient.ts +++ b/nodejs/test/e2e/harness/ahpClient.ts @@ -39,10 +39,11 @@ export async function withDeadline(promise: Promise, label: string, ms = 3 } export async function connectAhp( - host: { url: string; token?: string }, + host: { url?: string; token?: string }, capabilities?: ClientCapabilities, clientId: string = randomUUID() ) { + assert(host.url, "A local listener URL is required for a WebSocket connection"); const url = new URL(host.url); if (host.token !== undefined) url.searchParams.set("tkn", host.token); const socket = new WebSocket(url, { handshakeTimeout: 10_000 }); diff --git a/nodejs/test/e2e/harness/globalSetup.ts b/nodejs/test/e2e/harness/globalSetup.ts new file mode 100644 index 0000000000..80f6c3bf34 --- /dev/null +++ b/nodejs/test/e2e/harness/globalSetup.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { build } from "esbuild"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import type { TestProject } from "vitest/node"; +import { CAPI_PROXY_BUNDLE } from "./proxyBundleContext"; + +export default async function setup(project: TestProject): Promise { + const { testFiles } = await project.globTestFiles(); + if (!testFiles.some((file) => file.replaceAll("\\", "/").includes("/test/e2e/"))) { + return; + } + + const serverPath = join(project.tmpDir, "capi-proxy", "server.mjs"); + const buildBundle = async () => { + await build({ + entryPoints: [ + fileURLToPath(new URL("../../../../test/harness/server.ts", import.meta.url)), + ], + outfile: serverPath, + bundle: true, + platform: "node", + format: "esm", + target: "node20", + // Bundled CommonJS dependencies still require Node built-ins dynamically. + banner: { + js: 'import { createRequire } from "node:module"; const require = createRequire(import.meta.url);', + }, + }); + }; + + await buildBundle(); + project.provide(CAPI_PROXY_BUNDLE, serverPath); + project.onTestsRerun(buildBundle); +} diff --git a/nodejs/test/e2e/harness/proxyBundleContext.ts b/nodejs/test/e2e/harness/proxyBundleContext.ts new file mode 100644 index 0000000000..ed62e6f03a --- /dev/null +++ b/nodejs/test/e2e/harness/proxyBundleContext.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +export const CAPI_PROXY_BUNDLE = "capiProxyBundle"; + +declare module "vitest" { + export interface ProvidedContext { + capiProxyBundle: string; + } +} diff --git a/nodejs/test/e2e/harness/runtimeHostTopology.ts b/nodejs/test/e2e/harness/runtimeHostTopology.ts index 9a8b9d3188..1676af9e10 100644 --- a/nodejs/test/e2e/harness/runtimeHostTopology.ts +++ b/nodejs/test/e2e/harness/runtimeHostTopology.ts @@ -6,7 +6,7 @@ import assert from "node:assert/strict"; import { readFile, readdir, readlink } from "node:fs/promises"; import { connect } from "node:net"; -type Listener = { url: string; token?: string }; +type Listener = { url?: string; token?: string }; /** Verify the listener's socket and loaded provider, independently of the RPC PID. */ export async function assertRuntimeListener( @@ -14,6 +14,7 @@ export async function assertRuntimeListener( runtimePid: number, artifacts: { runtimePath: string; providerPath: string; bundled?: boolean; embedded?: boolean } ) { + assert(host.url, "A local listener URL is required for topology checks"); assert.equal(await readlink(`/proc/${runtimePid}/exe`), artifacts.runtimePath); const maps = await readFile(`/proc/${runtimePid}/maps`, "utf8"); assert(maps.includes(artifacts.providerPath), "Runtime must load the source-built provider"); @@ -73,6 +74,14 @@ export async function assertRuntimeListener( } const port = Number(new URL(host.url).port); + assert( + (await listeningPorts(runtimePid)).has(port), + "The runtime PID must own the listening TCP socket, not merely advertise its PID" + ); +} + +/** Read the TCP listeners owned by this process, rather than its network namespace. */ +export async function listeningPorts(runtimePid: number): Promise> { const sockets = new Set(); for (const fd of await readdir(`/proc/${runtimePid}/fd`)) { const target = await readlink(`/proc/${runtimePid}/fd/${fd}`).catch( @@ -87,25 +96,20 @@ export async function assertRuntimeListener( const tables = await Promise.all( ["tcp", "tcp6"].map((name) => readFile(`/proc/${runtimePid}/net/${name}`, "utf8")) ); - assert( - tables.some((table) => - table - .split("\n") - .slice(1) - .some((line) => { - const fields = line.trim().split(/\s+/); - return ( - fields[3] === "0A" && - parseInt(fields[1]?.split(":")[1] ?? "", 16) === port && - sockets.has(fields[9]) - ); - }) - ), - "The runtime PID must own the listening TCP socket, not merely advertise its PID" - ); + const ports = new Set(); + for (const table of tables) { + for (const line of table.split("\n").slice(1)) { + const fields = line.trim().split(/\s+/); + if (fields[3] === "0A" && sockets.has(fields[9])) { + ports.add(parseInt(fields[1].split(":")[1], 16)); + } + } + } + return ports; } export async function assertListenerClosed(host: Listener, runtimePid: number | false) { + assert(host.url, "A local listener URL is required for closure checks"); const url = new URL(host.url); const refusal = await new Promise((resolve, reject) => { const socket = connect({ diff --git a/nodejs/test/e2e/harness/sdkTestContext.test.ts b/nodejs/test/e2e/harness/sdkTestContext.test.ts new file mode 100644 index 0000000000..3503e1080f --- /dev/null +++ b/nodejs/test/e2e/harness/sdkTestContext.test.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { existsSync } from "node:fs"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { rimraf } from "rimraf"; +import { afterAll, describe, expect, it, onTestFinished, vi } from "vitest"; +import { createSdkTestContext } from "./sdkTestContext"; + +vi.mock("./CapiProxy", () => ({ + CapiProxy: class { + start = vi.fn().mockResolvedValue("http://127.0.0.1:1"); + getProxyEnv = vi.fn().mockReturnValue({}); + setCopilotUserByToken = vi.fn().mockResolvedValue(undefined); + updateConfig = vi.fn().mockResolvedValue(undefined); + stop = vi.fn().mockResolvedValue(undefined); + }, +})); + +vi.mock("rimraf", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, rimraf: vi.fn(actual.rimraf) }; +}); + +describe("SDK test context cleanup", async () => { + let workDir: string; + afterAll(() => { + expect(rimraf).toHaveBeenCalled(); + expect(existsSync(workDir)).toBe(false); + }); + const context = await createSdkTestContext(); + workDir = context.workDir; + + it("keeps working files available until test-owned client cleanup finishes", async () => { + const client = context.createClient(); + const logsDir = join(workDir, "logs"); + const logPath = join(logsDir, "client.log"); + onTestFinished(async () => { + try { + expect(rimraf).not.toHaveBeenCalled(); + expect(await readFile(logPath, "utf8")).toBe("client is running"); + await writeFile(logPath, "client is stopped"); + } finally { + await client.stop(); + } + }); + await mkdir(logsDir); + await writeFile(logPath, "client is running"); + }); +}); diff --git a/nodejs/test/e2e/harness/sdkTestContext.ts b/nodejs/test/e2e/harness/sdkTestContext.ts index 479443e651..fd755c0289 100644 --- a/nodejs/test/e2e/harness/sdkTestContext.ts +++ b/nodejs/test/e2e/harness/sdkTestContext.ts @@ -3,16 +3,19 @@ *--------------------------------------------------------------------------------------------*/ import fs, { realpathSync } from "fs"; -import { rm } from "fs/promises"; +import { readdir, rm } from "fs/promises"; +import { createInterface } from "node:readline"; import os from "os"; import { basename, dirname, join, resolve } from "path"; import { rimraf } from "rimraf"; import { fileURLToPath } from "url"; -import { afterAll, afterEach, beforeEach, onTestFailed, TestContext } from "vitest"; +import { afterAll, afterEach, beforeEach, onTestFailed, onTestFinished, TestContext } from "vitest"; import { CopilotClient, CopilotClientOptions, RuntimeConnection } from "../../../src"; +import type { ResumeSessionConfig, SessionConfig } from "../../../src"; import { CapiProxy } from "./CapiProxy"; import { formatError, retry } from "./sdkTestHelper"; import { resolvePreparedRuntimePath } from "../../../scripts/prepare-runtime.js"; +import { testBackend, withTestBackend } from "./testBackend"; export const isCI = process.env.GITHUB_ACTIONS === "true"; export const DEFAULT_GITHUB_TOKEN = "fake-token-for-e2e-tests"; @@ -60,11 +63,13 @@ export async function createSdkTestContext({ logLevel, useStdio, copilotClientOptions, + modelNames, }: { logLevel?: "error" | "none" | "warning" | "info" | "debug" | "all"; cliPath?: string; useStdio?: boolean; copilotClientOptions?: CopilotClientOptions; + modelNames?: Record; } = {}) { const homeDir = realpathSync(fs.mkdtempSync(join(os.tmpdir(), "copilot-test-config-"))); const copilotHomeDir = realpathSync(fs.mkdtempSync(join(os.tmpdir(), "copilot-test-home-"))); @@ -159,6 +164,16 @@ export async function createSdkTestContext({ const mergedEnv = { ...env, ...userEnv }; + class TestClient extends CopilotClient { + override createSession(config: SessionConfig) { + return super.createSession(withTestBackend(config, testBackend, proxyUrl)); + } + + override resumeSession(sessionId: string, config: ResumeSessionConfig) { + return super.resumeSession(sessionId, withTestBackend(config, testBackend, proxyUrl)); + } + } + // The in-process (FFI) transport loads the runtime into this test host process, // and its worker inherits this process's ambient environment rather than a // per-client env block (see https://github.com/github/copilot-sdk/issues/1934). @@ -210,7 +225,7 @@ export async function createSdkTestContext({ } const effectiveInProcess = effectiveConnection.kind === "inprocess"; - return new CopilotClient({ + return new TestClient({ // The in-process transport rejects a per-client workingDirectory (it would have to // mutate the shared host process cwd). Instead the harness changes this process's // cwd to workDir around the in-process worker's startup (see beforeEach below), so @@ -224,7 +239,7 @@ export async function createSdkTestContext({ env: effectiveInProcess ? undefined : mergedEnv, logLevel: logLevel || "error", connection: effectiveConnection, - gitHubToken: authTokenToUse, + gitHubToken: effectiveConnection.kind === "uri" ? undefined : authTokenToUse, ...rest, }); } @@ -245,8 +260,41 @@ export async function createSdkTestContext({ // Wire up to Vitest lifecycle beforeEach(async (testContext) => { // Must be inside beforeEach - vitest requires test context - onTestFailed(() => { + onTestFailed(async () => { anyTestFailed = true; + // Preserve only the bounded, metadata-only runtime diagnostics before afterAll removes the home. + if (logLevel === "debug") { + try { + const logDir = join(copilotHomeDir, "logs"); + for (const file of await readdir(logDir)) { + if (!file.endsWith(".log")) continue; + const lines = createInterface({ + input: fs.createReadStream(join(logDir, file)), + }); + const diagnostics: string[] = []; + for await (const line of lines) { + if (!/^\S+ \[DEBUG\] \[rust:sdk_diagnostics\] /.test(line)) continue; + diagnostics.push(line); + if (diagnostics.length > 200) diagnostics.shift(); + } + console.error( + `[SDK runtime diagnostics: ${file}; last ${diagnostics.length} matching lines]\n${diagnostics.join("\n")}` + ); + } + } catch (error) { + console.error(`Could not read SDK runtime diagnostics: ${formatError(error)}`); + } + } + }); + // Finished hooks run in reverse order, so test-owned clients stop before files are removed. + onTestFinished(async () => { + // The in-process runtime retains open state files until afterAll shuts it down. + // Keep its isolated home intact while it is alive; removing open files on POSIX + // can leave later tests using unlinked database state. + const cleanupPaths = isInProcess + ? [join(workDir, "*")] + : [join(homeDir, "*"), join(workDir, "*")]; + await rimraf(cleanupPaths, { glob: true }); }); // Mirror this context's environment onto the real process for in-process @@ -271,6 +319,7 @@ export async function createSdkTestContext({ await openAiEndpoint.updateConfig({ filePath: getTrafficCapturePath(testContext), workDir, + modelNames, testInfo: { file: testContext.task.file.filepath, line: testContext.task.location?.line, @@ -293,13 +342,6 @@ export async function createSdkTestContext({ process.chdir(restoreCwd); restoreCwd = undefined; } - // The in-process runtime retains open state files until afterAll shuts it down. - // Keep its isolated home intact while it is alive; removing open files on POSIX - // can leave later tests using unlinked database state. - const cleanupPaths = isInProcess - ? [join(workDir, "*")] - : [join(homeDir, "*"), join(workDir, "*")]; - await rimraf(cleanupPaths, { glob: true }); }); afterAll(async () => { diff --git a/nodejs/test/e2e/harness/sdkTestHelper.ts b/nodejs/test/e2e/harness/sdkTestHelper.ts index 9cca10388b..965857a4f1 100644 --- a/nodejs/test/e2e/harness/sdkTestHelper.ts +++ b/nodejs/test/e2e/harness/sdkTestHelper.ts @@ -23,11 +23,11 @@ export async function stopChildProcess(child: ChildProcess): Promise { } } -function hasChildExited(child: ChildProcess): boolean { +export function hasChildExited(child: ChildProcess): boolean { return child.exitCode !== null || child.signalCode !== null; } -function waitForChildExit(child: ChildProcess, timeoutMs: number): Promise { +export function waitForChildExit(child: ChildProcess, timeoutMs?: number): Promise { if (hasChildExited(child)) { return Promise.resolve(true); } @@ -44,7 +44,8 @@ function waitForChildExit(child: ChildProcess, timeoutMs: number): Promise finish(true); - const timeout = setTimeout(() => finish(false), timeoutMs); + const timeout = + timeoutMs === undefined ? undefined : setTimeout(() => finish(false), timeoutMs); child.once("exit", onExit); if (hasChildExited(child)) { diff --git a/nodejs/test/e2e/harness/testBackend.ts b/nodejs/test/e2e/harness/testBackend.ts new file mode 100644 index 0000000000..21ab4f8b3e --- /dev/null +++ b/nodejs/test/e2e/harness/testBackend.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import type { ProviderConfig, SessionConfig } from "../../../src"; +import type { ReplayBackend } from "../../../../test/harness/replayingCapiProxy"; + +export function parseTestBackend(value: string | undefined): ReplayBackend { + const backend = value?.trim().toLowerCase() || "capi"; + switch (backend) { + case "capi": + case "anthropic-messages": + case "openai-responses": + case "openai-completions": + return backend; + default: + throw new Error(`Unsupported COPILOT_SDK_E2E_BACKEND: ${value}`); + } +} + +export const testBackend = parseTestBackend(process.env.COPILOT_SDK_E2E_BACKEND); +export const isByokBackend = testBackend !== "capi"; + +export function withTestBackend>( + config: T, + backend: ReplayBackend, + proxyUrl: string +): T { + if (backend === "capi" || config.provider !== undefined || config.providers !== undefined) { + return config; + } + + const model = + config.model ?? (backend === "anthropic-messages" ? "claude-sonnet-5" : "gpt-4.1"); + const provider: ProviderConfig = { + type: backend === "anthropic-messages" ? "anthropic" : "openai", + wireApi: + backend === "anthropic-messages" + ? undefined + : backend === "openai-responses" + ? "responses" + : "completions", + baseUrl: proxyUrl, + bearerToken: "fake-byok-credential-for-e2e-tests", + modelId: model, + wireModel: model, + }; + return { ...config, model, provider }; +} diff --git a/nodejs/test/e2e/mcp_cached_oauth_overlap.e2e.test.ts b/nodejs/test/e2e/mcp_cached_oauth_overlap.e2e.test.ts new file mode 100644 index 0000000000..ecbacd34fd --- /dev/null +++ b/nodejs/test/e2e/mcp_cached_oauth_overlap.e2e.test.ts @@ -0,0 +1,415 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { execFileSync, spawn } from "node:child_process"; +import { mkdtemp, realpath, rm } from "node:fs/promises"; +import { createServer } from "node:http"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { createInterface } from "node:readline"; +import { text } from "node:stream/consumers"; +import { fileURLToPath } from "node:url"; +import { describe, expect, onTestFinished, test } from "vitest"; +import { + approveAll, + CopilotClient, + RuntimeConnection, + type CopilotSession, + type McpAuthHandler, + type MCPServerConfig, + type McpAuthRequest, + type NamedProviderConfig, + type ProviderModelConfig, + type SessionEvent, +} from "../../src/index.js"; +import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js"; +import { stopChildProcess, waitForCondition } from "./harness/sdkTestHelper.js"; + +const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url))); +const TEST_MCP_OAUTH_SERVER = resolve(__dirname, "../../../test/harness/test-mcp-oauth-server.mjs"); +const TEST_MCP_SERVER = resolve(__dirname, "../../../test/harness/test-mcp-server.mjs"); +const EXPECTED_TOKEN = "sdk-cached-overlap-token"; +const STEP_TIMEOUT_MS = 60_000; + +type LiveCatalog = "unchanged" | "removed" | "changed-schema"; + +describe("cached MCP OAuth overlap", async () => { + const { env: harnessEnv } = await createSdkTestContext(); + + test.for(["unchanged", "removed", "changed-schema"] as const)( + "holds a cached model call across authentication completion (%s)", + { timeout: 180_000 }, + async (liveCatalog: LiveCatalog) => { + const directory = await mkdtemp(join(tmpdir(), "mcp-cached-oauth-overlap-")); + const clients: CopilotClient[] = []; + let model: ReturnType | undefined; + let startingOAuthServer: ReturnType | undefined; + onTestFinished(async () => { + const errors: unknown[] = []; + const [oauthStartup] = await Promise.allSettled( + startingOAuthServer ? [startingOAuthServer] : [] + ); + for (const client of clients.reverse()) { + try { + errors.push(...(await client.stop())); + } catch (error) { + errors.push(error); + } + } + model?.closeAllConnections(); + if (model?.listening) { + try { + await new Promise((resolvePromise, reject) => { + model?.close((error) => (error ? reject(error) : resolvePromise())); + }); + } catch (error) { + errors.push(error); + } + } + if (oauthStartup?.status === "fulfilled") { + try { + await oauthStartup.value.stop(); + } catch (error) { + errors.push(error); + } + } + try { + await rm(directory, { + recursive: true, + force: true, + maxRetries: 10, + retryDelay: 100, + }); + } catch (error) { + errors.push(error); + } + if (errors.length) { + throw new AggregateError(errors, "Cached OAuth E2E cleanup failed"); + } + }); + + const workdir = await realpath(directory); + execFileSync("git", ["init", "--quiet", workdir], { windowsHide: true }); + const events: SessionEvent[] = []; + const advertisedTools: string[][] = []; + let modelFailure: Error | undefined; + let modelRequests = 0; + const modelEntered = Promise.withResolvers(); + const releaseModel = Promise.withResolvers(); + model = createServer((request, response) => { + void (async () => { + const body = JSON.parse(await text(request)) as { + tools?: Array<{ function?: { name: string } }>; + }; + const tools = + body.tools?.flatMap((tool) => + tool.function ? [tool.function.name] : [] + ) ?? []; + advertisedTools.push(tools); + const step = modelRequests++; + if (step === 0) { + modelEntered.resolve(); + await releaseModel.promise; + const workiqTool = tools.find((name) => name.endsWith("-whoami")); + if (!workiqTool) throw new Error("Cached WorkIQ tool was not advertised"); + respondModel(response, { + role: "assistant", + content: null, + tool_calls: [ + { + id: "cached-oauth-call", + type: "function", + function: { name: workiqTool, arguments: "{}" }, + }, + ], + }); + return; + } + if (step === 1) { + respondModel(response, { + role: "assistant", + content: `CACHED_OAUTH_${liveCatalog.toUpperCase()}_DONE`, + }); + return; + } + throw new Error(`Unexpected model request ${step}`); + })().catch((error: unknown) => { + modelFailure = error instanceof Error ? error : new Error(String(error)); + response.writeHead(500).end(); + }); + }); + startingOAuthServer = startOAuthMcpServer(); + const oauthServer = await startingOAuthServer; + await new Promise((resolvePromise, reject) => { + model.once("error", reject); + model.listen(0, "127.0.0.1", resolvePromise); + }); + const modelAddress = model.address(); + if (!modelAddress || typeof modelAddress === "string") { + throw new Error("No model fixture port"); + } + + const providers: NamedProviderConfig[] = [ + { + name: "local", + type: "openai", + baseUrl: `http://127.0.0.1:${modelAddress.port}`, + apiKey: "test", + wireApi: "completions", + }, + ]; + const models: ProviderModelConfig[] = [ + { id: "model", provider: "local", modelId: "test-model", wireModel: "test-model" }, + ]; + const cacheHome = join(workdir, "cache"); + const makeClient = (profile: string) => { + const client = new CopilotClient({ + workingDirectory: workdir, + env: { + ...harnessEnv, + COPILOT_HOME: join(workdir, profile), + COPILOT_CACHE_HOME: cacheHome, + COPILOT_DISABLE_KEYTAR: "1", + COPILOT_MCP_APPS: "false", + COPILOT_MCP_TOOL_CACHE: "true", + TOOL_SEARCH_DISABLED: "1", + }, + gitHubToken: DEFAULT_GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ + path: process.env.COPILOT_CLI_PATH, + }), + }); + clients.push(client); + return client; + }; + const sessionOptions = (onMcpAuthRequest: McpAuthHandler) => ({ + onPermissionRequest: approveAll, + onMcpAuthRequest, + workingDirectory: workdir, + mcpServers: { + workiq: { + type: "http", + url: `${oauthServer.url}/mcp`, + tools: ["*"], + oauthClientId: "sdk-cached-overlap-client", + oauthPublicClient: true, + } as unknown as MCPServerConfig, + peer: { + type: "local", + command: process.execPath, + args: [TEST_MCP_SERVER, "--server-name", "peer"], + tools: ["*"], + } as MCPServerConfig, + }, + providers, + models, + model: "local/model", + }); + const project = async (session: CopilotSession) => { + await session.rpc.tools.initializeAndValidate(); + const metadata = await session.rpc.tools.getCurrentMetadata(); + return (metadata.tools ?? []).map((tool) => tool.name); + }; + + const seedClient = makeClient("seed-profile"); + const seedSession = await seedClient.createSession( + sessionOptions(async () => ({ + kind: "token", + accessToken: EXPECTED_TOKEN, + tokenType: "Bearer", + expiresIn: 3600, + })) + ); + await waitForMcpServerStatus(seedSession, "workiq", "connected"); + const seededTools = await project(seedSession); + expect(seededTools).toContain("workiq-whoami"); + expect(seededTools).toContain("peer-get_env"); + await seedClient.deleteSession(seedSession.sessionId); + const seedStopErrors = await seedClient.stop(); + expect(seedStopErrors).toEqual([]); + clients.splice(clients.indexOf(seedClient), 1); + + await oauthServer.setToolMode(liveCatalog); + const authEntered = Promise.withResolvers(); + let credentialsAvailable = false; + const liveClient = makeClient("live-profile"); + const session = await liveClient.createSession( + sessionOptions(async (request) => { + authEntered.resolve(request); + if (!credentialsAvailable) { + return { kind: "cancelled" }; + } + return { + kind: "token", + accessToken: EXPECTED_TOKEN, + tokenType: "Bearer", + expiresIn: 3600, + }; + }) + ); + session.on((event) => events.push(event)); + + const cachedTools = await project(session); + expect(cachedTools).toContain("workiq-whoami"); + expect(cachedTools).toContain("peer-get_env"); + const turn = session.send({ prompt: "Run the cached WorkIQ identity tool." }); + await Promise.all([modelEntered.promise, authEntered.promise]); + await waitForMcpServerStatus(session, "workiq", "needs-auth"); + expect(advertisedTools[0]).toContain("workiq-whoami"); + expect(advertisedTools[0]).toContain("peer-get_env"); + + credentialsAvailable = true; + try { + await session.rpc.mcp.oauth.authenticationStateChanged({ serverName: "workiq" }); + } catch (error) { + expect(String(error)).toContain( + 'MCP server "workiq" still requires authentication' + ); + } + await waitForMcpServerStatus(session, "workiq", "connected"); + releaseModel.resolve(); + await turn; + + const finalReply = `CACHED_OAUTH_${liveCatalog.toUpperCase()}_DONE`; + await waitForCondition( + () => { + if (modelFailure) throw modelFailure; + return events.some( + (event) => + event.type === "assistant.message" && event.data.content === finalReply + ); + }, + { timeoutMs: STEP_TIMEOUT_MS, timeoutMessage: `No reply ${finalReply}` } + ); + const completion = events.find( + (event) => + event.type === "tool.execution_complete" && + event.data.toolCallId === "cached-oauth-call" + ); + expect(completion?.type).toBe("tool.execution_complete"); + if (completion?.type !== "tool.execution_complete") { + throw new Error("No cached OAuth tool completion"); + } + expect(completion.data.success).toBe(liveCatalog === "unchanged"); + expect(advertisedTools[1]).toContain("peer-get_env"); + + const requests = await oauthServer.requests(); + const toolCalls = requests.filter((request) => { + if (!request.body) return false; + const body = JSON.parse(request.body) as { method?: string }; + return body.method === "tools/call"; + }); + expect(toolCalls).toHaveLength(liveCatalog === "unchanged" ? 1 : 0); + if (liveCatalog !== "unchanged") { + expect(completion.data.error?.message).toContain( + 'MCP tool catalog changed before tool "workiq-whoami" could be invoked' + ); + } + } + ); +}); + +function respondModel( + response: import("node:http").ServerResponse, + message: Record +): void { + response.writeHead(200, { "content-type": "application/json" }); + response.end( + JSON.stringify({ + id: "cached-oauth-completion", + object: "chat.completion", + created: 0, + model: "test-model", + choices: [ + { + index: 0, + message, + finish_reason: "tool_calls" in message ? "tool_calls" : "stop", + }, + ], + usage: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20 }, + }) + ); +} + +async function waitForMcpServerStatus( + session: CopilotSession, + serverName: string, + expectedStatus: string +): Promise { + let lastStatus = ""; + await waitForCondition( + async () => { + const result = await session.rpc.mcp.list(); + const server = result.servers.find((entry) => entry.name === serverName); + lastStatus = server?.status ?? ""; + return server?.status === expectedStatus; + }, + { + timeoutMs: STEP_TIMEOUT_MS, + intervalMs: 200, + timeoutMessage: `${serverName} did not reach ${expectedStatus}; last status was ${lastStatus}`, + } + ); +} + +async function startOAuthMcpServer(): Promise<{ + url: string; + requests(): Promise>; + setToolMode(mode: LiveCatalog): Promise; + stop(): Promise; +}> { + const child = spawn(process.execPath, [TEST_MCP_OAUTH_SERVER], { + env: { ...process.env, EXPECTED_TOKEN }, + stdio: ["ignore", "pipe", "pipe"], + }); + const stderr: string[] = []; + child.stderr.on("data", (chunk) => stderr.push(String(chunk))); + let url: string; + try { + url = await new Promise((resolvePromise, reject) => { + const lines = createInterface({ input: child.stdout }); + const timeout = setTimeout(() => { + lines.close(); + reject(new Error(`Timed out waiting for OAuth server. ${stderr.join("")}`)); + }, 10_000); + child.once("exit", (code, signal) => { + clearTimeout(timeout); + lines.close(); + reject( + new Error( + `OAuth server exited before listening. code=${code} signal=${signal} ${stderr.join("")}` + ) + ); + }); + lines.on("line", (line) => { + const match = /^Listening: (.+)$/.exec(line); + if (!match) return; + clearTimeout(timeout); + lines.close(); + resolvePromise(match[1]); + }); + }); + } catch (error) { + await stopChildProcess(child); + throw error; + } + return { + url, + requests: async () => { + const response = await fetch(`${url}/__requests`); + if (!response.ok) throw new Error(`Failed to fetch requests: ${response.status}`); + return response.json(); + }, + setToolMode: async (mode) => { + const response = await fetch(`${url}/__tool-mode`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ mode }), + }); + if (!response.ok) throw new Error(`Failed to set tool mode: ${response.status}`); + }, + stop: () => stopChildProcess(child), + }; +} diff --git a/nodejs/test/e2e/mcp_catalog_recovery.e2e.test.ts b/nodejs/test/e2e/mcp_catalog_recovery.e2e.test.ts new file mode 100644 index 0000000000..c9f2a8b4fd --- /dev/null +++ b/nodejs/test/e2e/mcp_catalog_recovery.e2e.test.ts @@ -0,0 +1,355 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** SDK coverage using a real stdio MCP server and a local, deterministic model. */ +import { execFileSync } from "node:child_process"; +import { mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises"; +import { createServer } from "node:http"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { text } from "node:stream/consumers"; +import { fileURLToPath } from "node:url"; +import { describe, expect, onTestFinished, test } from "vitest"; +import { + approveAll, + CopilotClient, + RuntimeConnection, + type CopilotSession, + type NamedProviderConfig, + type ProviderModelConfig, + type SessionEvent, +} from "../../src/index.js"; +import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; + +const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url))); +const CATALOG_RECOVERY_SERVER = resolve( + __dirname, + "../../../test/harness/test-mcp-catalog-recovery-server.mjs" +); +const STEP_TIMEOUT_MS = 60_000; + +describe("MCP catalog recovery", async () => { + const { env: harnessEnv } = await createSdkTestContext(); + + test.for(["live", "cached-startup"] as const)( + "MCP tools remain callable after timeouts and failed discovery recovers without a restart (%s)", + { timeout: 180_000 }, + async (scenario) => { + const cachedStartup = scenario === "cached-startup"; + const directory = await mkdtemp(join(tmpdir(), "mcp-catalog-recovery-")); + let failure: Error | undefined; + let modelRequests = 0; + const advertisedTools: string[][] = []; + const events: SessionEvent[] = []; + const model = createServer((request, response) => { + void (async () => { + const body = JSON.parse(await text(request)) as { + tools?: Array<{ function?: { name: string } }>; + }; + advertisedTools.push( + body.tools?.flatMap((tool) => + tool.function ? [tool.function.name] : [] + ) ?? [] + ); + const step = modelRequests++; + const toolName = new Map([ + [0, "probe-ping"], + [1, "probe-stall"], + [3, "probe-refresh"], + [5, "probe-recovered"], + ]).get(step); + const reply = new Map([ + [2, "TIMEOUT_TURN_DONE"], + [4, "DEGRADED_TURN_DONE"], + [6, "CATALOG_RECOVERY_DONE"], + [7, "DISABLED_TURN_DONE"], + ]).get(step); + if (!toolName && !reply) throw new Error(`Unexpected model request ${step}`); + const message = toolName + ? { + role: "assistant", + content: null, + tool_calls: [ + { + id: `catalog-call-${step}`, + type: "function", + function: { name: toolName, arguments: "{}" }, + }, + ], + } + : { + role: "assistant", + content: reply, + }; + response.writeHead(200, { "content-type": "application/json" }); + response.end( + JSON.stringify({ + id: `completion-${step}`, + object: "chat.completion", + created: 0, + model: "test-model", + choices: [ + { + index: 0, + message, + finish_reason: toolName ? "tool_calls" : "stop", + }, + ], + usage: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20 }, + }) + ); + })().catch((error: unknown) => { + failure = error instanceof Error ? error : new Error(String(error)); + response.writeHead(500).end(); + }); + }); + + const workdir = await realpath(directory); + execFileSync("git", ["init", "--quiet", workdir], { windowsHide: true }); + const trafficPath = join(workdir, "traffic.log"); + const startupReleasePath = join(workdir, "startup-release"); + const recoveryReleasePath = join(workdir, "recovery-release"); + await writeFile(startupReleasePath, "release"); + const traffic = async () => (await readFile(trafficPath, "utf8")).trim().split(/\r?\n/); + await new Promise((resolve, reject) => { + model.once("error", reject); + model.listen(0, "127.0.0.1", resolve); + }); + const address = model.address(); + if (!address || typeof address === "string") throw new Error("No model fixture port"); + + const client = new CopilotClient({ + workingDirectory: workdir, + env: { + ...harnessEnv, + TOOL_SEARCH_DISABLED: "1", + COPILOT_MCP_TOOL_CACHE: String(cachedStartup), + // MCP Apps performs separate post-call metadata probes, outside catalog recovery. + COPILOT_MCP_APPS: "false", + COPILOT_CACHE_HOME: join(workdir, "cache"), + COPILOT_DISABLE_KEYTAR: "1", + }, + gitHubToken: DEFAULT_GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + }); + + onTestFinished(async () => { + const errors: unknown[] = []; + try { + // stop() reports teardown failures by returning them, not by throwing. + errors.push(...(await client.stop())); + } catch (error) { + errors.push(error); + } + model.closeAllConnections(); + try { + if (model.listening) { + await new Promise((resolve, reject) => { + model.close((error) => (error ? reject(error) : resolve())); + }); + } + } catch (error) { + errors.push(error); + } + try { + await rm(directory, { + recursive: true, + force: true, + maxRetries: 10, + retryDelay: 100, + }); + } catch (error) { + errors.push(error); + } + if (errors.length) + throw new AggregateError(errors, "MCP catalog fixture cleanup failed"); + }); + + const providers: NamedProviderConfig[] = [ + { + name: "local", + type: "openai", + baseUrl: `http://127.0.0.1:${address.port}`, + apiKey: "test", + wireApi: "completions", + }, + ]; + const models: ProviderModelConfig[] = [ + { id: "model", provider: "local", modelId: "test-model", wireModel: "test-model" }, + ]; + const createProbeSession = async (): Promise => { + const created = await client.createSession({ + onPermissionRequest: approveAll, + workingDirectory: workdir, + mcpServers: { + probe: { + type: "local", + command: process.execPath, + args: [ + CATALOG_RECOVERY_SERVER, + trafficPath, + startupReleasePath, + recoveryReleasePath, + ], + tools: ["*"], + timeout: 2_500, + }, + }, + providers, + models, + model: "local/model", + }); + created.on((event) => { + events.push(event); + }); + return created; + }; + + let session = await createProbeSession(); + const project = async () => { + await session.rpc.tools.initializeAndValidate(); + const metadata = await session.rpc.tools.getCurrentMetadata(); + return (metadata.tools ?? []).map((tool) => tool.name); + }; + const sendTurn = async (prompt: string, reply: string) => { + const idleCount = events.filter((event) => event.type === "session.idle").length; + await session.send({ prompt }); + // Validate the final assistant response arrived (guards against a lost turn) + await waitForCondition( + () => { + if (failure) throw failure; + return events.some( + (event) => + event.type === "assistant.message" && event.data.content === reply + ); + }, + { timeoutMs: STEP_TIMEOUT_MS, timeoutMessage: `No assistant reply ${reply}` } + ); + await waitForCondition( + () => + events.filter((event) => event.type === "session.idle").length > idleCount, + { timeoutMs: STEP_TIMEOUT_MS, timeoutMessage: `Turn for ${reply} never idled` } + ); + }; + const pollTraffic = async (line: string, message: string) => + waitForCondition(async () => (await traffic()).includes(line), { + timeoutMs: STEP_TIMEOUT_MS, + timeoutMessage: message, + }); + + expect(await project()).toContain("probe-ping"); + if (cachedStartup) { + const previousSessionId = session.sessionId; + await client.deleteSession(previousSessionId); + await rm(startupReleasePath); + session = await createProbeSession(); + const cachedTools = await project(); + expect(cachedTools).toContain("probe-obsolete"); + expect(cachedTools).not.toContain("probe-recovered"); + await pollTraffic("startup-held", "Fixture never held its startup listing"); + await writeFile(startupReleasePath, "release"); + await pollTraffic("list-cancelled", "Held startup listing was never cancelled"); + const retryProjections: string[][] = []; + await waitForCondition( + async () => { + retryProjections.push(await project()); + return (await traffic()).includes("list-failed"); + }, + { timeoutMs: STEP_TIMEOUT_MS, timeoutMessage: "Discovery retry never failed" } + ); + for (const names of retryProjections) { + expect(names).toContain("probe-ping"); + expect(names).toContain("probe-obsolete"); + expect(names).not.toContain("probe-recovered"); + } + await writeFile(recoveryReleasePath, "release"); + // Do not invoke a tool or emit a notification that could mask a lost + // background failure; only subsequent catalog projections may recover. + await waitForCondition( + async () => { + const names = await project(); + expect(names).toContain("probe-ping"); + return names.includes("probe-recovered"); + }, + { + timeoutMs: STEP_TIMEOUT_MS, + timeoutMessage: "Catalog never recovered after the failed retry", + } + ); + } + await sendTurn("Call ping, then stall.", "TIMEOUT_TURN_DONE"); + await pollTraffic("tool-cancelled", "Timed-out tool call was never cancelled"); + const afterTimeout = await traffic(); + const stallIndex = afterTimeout.indexOf("tools/call:stall"); + expect(stallIndex).toBeGreaterThanOrEqual(0); + expect(afterTimeout.slice(stallIndex)).not.toContain("tools/list"); + expect(advertisedTools[2]).toContain("probe-ping"); + const completed = () => + events.filter((event) => event.type === "tool.execution_complete"); + expect(completed()[0].data).toMatchObject({ success: true }); + expect(completed()[1].data).toMatchObject({ success: false }); + expect(JSON.stringify(completed()[1].data)).toContain("Request timed out"); + + const cancelledListsBeforeRefresh = (await traffic()).filter( + (line) => line === "list-cancelled" + ).length; + await sendTurn("Call refresh.", "DEGRADED_TURN_DONE"); + expect(events.filter((event) => event.type === "session.error")).toEqual([]); + await waitForCondition( + () => + events.some( + (event) => + event.type === "session.warning" && + event.data.warningType === "mcp" && + event.data.message === + 'MCP server "probe" announced changed tools, but refreshing its catalog failed. Some tools may be unavailable or outdated; discovery will retry on the next turn.' + ), + { + timeoutMs: STEP_TIMEOUT_MS, + timeoutMessage: "No degraded-catalog MCP warning was emitted", + } + ); + expect(modelRequests).toBe(5); + expect(advertisedTools[4]).toContain("probe-ping"); + if (cachedStartup) { + expect(advertisedTools[4]).toContain("probe-recovered"); + } else { + expect(advertisedTools[4]).not.toContain("probe-recovered"); + } + await waitForCondition( + async () => + (await traffic()).filter((line) => line === "list-cancelled").length === + cancelledListsBeforeRefresh + 1, + { + timeoutMs: STEP_TIMEOUT_MS, + timeoutMessage: "The announced-change listing was never cancelled", + } + ); + + // The fixture recovers after cancellation without another notification. + await waitForCondition(async () => (await project()).includes("probe-recovered"), { + timeoutMs: STEP_TIMEOUT_MS, + timeoutMessage: "Catalog never recovered after the degraded refresh", + }); + await sendTurn("Call recovered.", "CATALOG_RECOVERY_DONE"); + expect(completed()).toHaveLength(4); + expect(completed()[3].data).toMatchObject({ success: true }); + expect(JSON.stringify(completed()[3].data)).toContain("RECOVERED_TOOL_REPLY"); + expect(advertisedTools[5]).toContain("probe-recovered"); + expect(advertisedTools[5]).not.toContain("probe-obsolete"); + const finalTraffic = await traffic(); + expect(finalTraffic.filter((line) => line === "started")).toHaveLength( + cachedStartup ? 2 : 1 + ); + expect(finalTraffic.filter((line) => line === "list-cancelled")).toHaveLength( + cachedStartup ? 2 : 1 + ); + + await session.rpc.mcp.disable({ serverName: "probe" }); + await sendTurn("Reply without tools.", "DISABLED_TURN_DONE"); + expect(advertisedTools[7].filter((name) => name.startsWith("probe-"))).toEqual([]); + } + ); +}); diff --git a/nodejs/test/e2e/mcp_instruction_recovery.e2e.test.ts b/nodejs/test/e2e/mcp_instruction_recovery.e2e.test.ts new file mode 100644 index 0000000000..299f173e4f --- /dev/null +++ b/nodejs/test/e2e/mcp_instruction_recovery.e2e.test.ts @@ -0,0 +1,252 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { createServer } from "node:http"; +import { describe, expect, it, onTestFinished } from "vitest"; +import { z } from "zod"; +import { approveAll, defineTool, type CopilotSession, type SessionEvent } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; + +const REPLACEMENT_INSTRUCTIONS = + "CATALOG_RECOVERY_INSTRUCTIONS: Use read_record to retrieve the current record."; +const RECONNECT_RESULT = "Catalog reconnected. Read both server records now."; + +describe("MCP catalog recovery", async () => { + const { copilotClient: client, openAiEndpoint } = await createSdkTestContext(); + + it.each([ + { serverName: "bluebird", surfaced: true, staleCall: false }, + { serverName: "workiq", surfaced: false, staleCall: true }, + ])( + "should recover $serverName instructions without withdrawing peer tools mid-turn", + { timeout: 120_000 }, + async ({ serverName, surfaced, staleCall }) => { + const changing = await startCatalogServer("CATALOG_RECORD"); + const healthy = await startCatalogServer("PEER_RECORD"); + let session: CopilotSession; + let reconnects = 0; + const reconnect = async () => { + reconnects++; + changing.instructions = REPLACEMENT_INSTRUCTIONS; + await session.rpc.mcp.restartServer({ serverName }); + await waitForConnected(session, serverName); + }; + session = await client.createSession({ + onPermissionRequest: approveAll, + availableTools: [ + ...(!staleCall ? ["reconnect_catalog"] : []), + `${serverName}-read_record`, + "healthy-read_record", + ], + mcpServers: { + [serverName]: { type: "http", url: changing.url, tools: ["*"] }, + healthy: { type: "http", url: healthy.url, tools: ["*"] }, + }, + hooks: staleCall + ? { + onPreMcpToolCall: async (input) => { + if ( + input.serverName === serverName && + input.toolName === "read_record" && + reconnects === 0 + ) { + // Invalidate the selected catalog before its invocation dispatches. + await reconnect(); + } + }, + } + : undefined, + tools: staleCall + ? [] + : [ + defineTool("reconnect_catalog", { + description: + "Reconnect the changing catalog before reading either server.", + parameters: z.object({}), + handler: async () => { + await reconnect(); + return RECONNECT_RESULT; + }, + }), + ], + }); + onTestFinished(() => session.disconnect()); + const events: SessionEvent[] = []; + session.on((event) => events.push(event)); + await waitForConnected(session, serverName); + await waitForConnected(session, "healthy"); + + // The instructions change at an awaited tool boundary, not during setup or a new turn. + // Tool definitions stay identical, exercising the instruction-only refresh fast path. + const response = await session.sendAndWait({ + prompt: + (staleCall + ? `Call read_record on ${serverName}. If it reports a changed tool catalog, retry it. ` + : `Call reconnect_catalog exactly once. After it returns, call read_record on ${serverName}. `) + + "then call read_record on healthy. Do not use other tools or invent their outputs. " + + "Finish by reporting both record values and CATALOG_RECOVERY_COMPLETE.", + }); + expect(response?.data.content).toContain("CATALOG_RECOVERY_COMPLETE"); + expect(response?.data.content).toContain("CATALOG_RECORD"); + expect(response?.data.content).toContain("PEER_RECORD"); + expect(reconnects).toBe(1); + expect(changing.calls).toBeGreaterThan(0); + expect(healthy.calls).toBeGreaterThan(0); + expect(changing.initializations).toBe(2); + expect(healthy.initializations).toBe(1); + if (staleCall) { + expect( + events.some( + (event) => + event.type === "tool.execution_complete" && + !event.data.success && + event.data.error?.message.includes( + "MCP tool catalog changed before tool" + ) + ), + "the first stale call must be refused before dispatch" + ).toBe(true); + expect(changing.calls, "only the recovered call may reach the MCP server").toBe(1); + } + + const exchanges = await openAiEndpoint.getExchanges(); + const initial = exchanges[0].request; + expect( + JSON.stringify(initial.messages.filter((message) => message.role === "system")) + ).not.toContain(REPLACEMENT_INSTRUCTIONS); + const afterReconnect = exchanges.filter(({ request }) => + request.messages.some( + (message) => + message.role === "tool" && + JSON.stringify(message.content).includes( + staleCall ? "MCP tool catalog changed before tool" : RECONNECT_RESULT + ) + ) + ); + expect( + afterReconnect.length, + "the same turn must continue after reconnect" + ).toBeGreaterThan(0); + for (const { request } of afterReconnect) { + const toolNames = request.tools + ?.filter((tool) => tool.type === "function") + .map((tool) => tool.function.name); + expect(toolNames).toContain(`${serverName}-read_record`); + expect(toolNames).toContain("healthy-read_record"); + const system = JSON.stringify( + request.messages.filter((message) => message.role === "system") + ); + if (surfaced) { + expect(system).toContain(REPLACEMENT_INSTRUCTIONS); + } else { + expect(system).not.toContain(REPLACEMENT_INSTRUCTIONS); + } + } + } + ); +}); + +async function waitForConnected(session: CopilotSession, serverName: string): Promise { + await waitForCondition( + async () => + (await session.rpc.mcp.list()).servers.some( + (server) => server.name === serverName && server.status === "connected" + ), + { + timeoutMs: 60_000, + timeoutMessage: `${serverName} should connect`, + } + ); +} + +async function startCatalogServer(record: string) { + const state = { instructions: "", calls: 0, initializations: 0, url: "" }; + const server = createServer(async (request, response) => { + if (request.method === "DELETE") { + response.writeHead(200).end(); + return; + } + if (request.method !== "POST") { + response.writeHead(405).end(); + return; + } + const chunks: Buffer[] = []; + for await (const chunk of request) { + chunks.push(Buffer.from(chunk)); + } + const message = JSON.parse(Buffer.concat(chunks).toString("utf8")) as { + id?: string | number; + method: string; + }; + if (message.id === undefined) { + response.writeHead(202).end(); + return; + } + let result: unknown; + switch (message.method) { + case "initialize": + state.initializations++; + result = { + protocolVersion: "2025-03-26", + capabilities: { tools: {} }, + serverInfo: { name: "catalog-recovery", version: "1" }, + ...(state.instructions ? { instructions: state.instructions } : {}), + }; + break; + case "tools/list": + result = { + tools: [ + { + name: "read_record", + description: "Read this server's current record.", + inputSchema: { + type: "object", + properties: {}, + additionalProperties: false, + }, + annotations: { readOnlyHint: true }, + }, + ], + }; + break; + case "tools/call": + state.calls++; + result = { content: [{ type: "text", text: record }], isError: false }; + break; + default: + response.writeHead(200, { "content-type": "application/json" }).end( + JSON.stringify({ + jsonrpc: "2.0", + id: message.id, + error: { code: -32601, message: "Method not found" }, + }) + ); + return; + } + response + .writeHead(200, { "content-type": "application/json" }) + .end(JSON.stringify({ jsonrpc: "2.0", id: message.id, result })); + }); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + server.off("error", reject); + resolve(); + }); + }); + onTestFinished( + () => + new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + server.closeAllConnections(); + }) + ); + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("Expected a TCP MCP server address"); + } + state.url = `http://127.0.0.1:${address.port}/mcp`; + return state; +} diff --git a/nodejs/test/e2e/mcp_legacy_sse_recovery.e2e.test.ts b/nodejs/test/e2e/mcp_legacy_sse_recovery.e2e.test.ts new file mode 100644 index 0000000000..6ba58b3d21 --- /dev/null +++ b/nodejs/test/e2e/mcp_legacy_sse_recovery.e2e.test.ts @@ -0,0 +1,422 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { execFileSync } from "node:child_process"; +import { createServer, type ServerResponse } from "node:http"; +import { join } from "node:path"; +import { describe, expect, it, onTestFinished } from "vitest"; +import { z } from "zod"; +import { + approveAll, + CopilotClient, + RuntimeConnection, + type PermissionRequest, + type SessionEvent, +} from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; + +const STEP_TIMEOUT_MS = 30_000; +const ACK_TIMEOUT_MS = 2_000; +const rpcRequest = z.object({ + method: z.string(), + id: z.union([z.string(), z.number()]).optional(), + params: z + .object({ + name: z.string().optional(), + arguments: z.object({ marker: z.string() }).optional(), + _meta: z + .object({ progressToken: z.union([z.string(), z.number()]).optional() }) + .optional(), + }) + .optional(), +}); + +/** A completed tool keeps sending progress while its POST acknowledgement stays gated. */ +function createLegacySseServer(replaceDescriptor: boolean) { + const streams = new Map(); + const closedStreams: number[] = []; + const initialized: number[] = []; + const listed: number[] = []; + const calls: Array<{ connection: number; id: string | number; marker: string }> = []; + const errors: unknown[] = []; + let nextConnection = 0; + let heldPost: { connection: number; closed: boolean; headersSent: boolean } | undefined; + let heldProgressTimer: ReturnType | undefined; + let progressSent = 0; + + const server = createServer((request, response) => { + const url = new URL(request.url ?? "", "http://127.0.0.1"); + if (request.method === "GET" && url.pathname === "/sse") { + const connection = ++nextConnection; + streams.set(connection, response); + response.on("close", () => { + closedStreams.push(connection); + streams.delete(connection); + }); + response.writeHead(200, { "Content-Type": "text/event-stream" }); + response.flushHeaders(); + response.write(`event: endpoint\ndata: /messages?connection=${connection}\n\n`); + return; + } + if (request.method !== "POST" || url.pathname !== "/messages") { + response.writeHead(404).end(); + return; + } + const connection = Number(url.searchParams.get("connection")); + request.setEncoding("utf8"); + let body = ""; + request.on("data", (chunk: string) => { + body += chunk; + }); + request.on("error", (error) => errors.push(error)); + request.on("end", () => { + try { + const frame = rpcRequest.parse(JSON.parse(body)); + const stream = streams.get(connection); + if (!stream) { + throw new Error(`POST for closed SSE connection ${connection}`); + } + if (frame.id === undefined) { + response.writeHead(202).end(); + return; + } + let result: unknown; + switch (frame.method) { + case "initialize": + initialized.push(connection); + result = { + protocolVersion: "2024-11-05", + capabilities: { tools: {} }, + serverInfo: { name: "legacy-sse-probe", version: "1.0" }, + }; + break; + case "tools/list": + listed.push(connection); + result = { + tools: [ + { + name: "ping", + description: + replaceDescriptor && connection > 1 + ? "Replacement writable ping" + : "Original read-only ping", + inputSchema: { + type: "object", + // The common projection fills only the root's missing properties object. + ...(connection > 1 ? { properties: {} } : {}), + allOf: [ + { + properties: { marker: { type: "string" } }, + required: ["marker"], + }, + ], + }, + annotations: { + readOnlyHint: !(replaceDescriptor && connection > 1), + }, + }, + ], + }; + break; + case "tools/call": { + if (frame.params?.name !== "ping" || !frame.params.arguments) { + throw new Error(`Unexpected tool call: ${body}`); + } + const marker = frame.params.arguments.marker; + calls.push({ connection, id: frame.id, marker }); + result = { + content: [{ type: "text", text: `SSE_REPLY_${connection}_${marker}` }], + isError: false, + }; + if (marker === "held") { + const token = frame.params._meta?.progressToken; + if (token === undefined) { + throw new Error("The held call did not carry a progress token"); + } + if (heldPost) { + throw new Error("The original gated frame was replayed"); + } + const held = { connection, closed: false, headersSent: false }; + heldPost = held; + response.on("close", () => { + held.headersSent = response.headersSent; + held.closed = true; + clearInterval(heldProgressTimer); + heldProgressTimer = undefined; + }); + stream.write( + `event: message\ndata: ${JSON.stringify({ + jsonrpc: "2.0", + id: frame.id, + result, + })}\n\n` + ); + heldProgressTimer = setInterval(() => { + if (!stream.destroyed && !stream.writableEnded) { + stream.write( + `event: message\ndata: ${JSON.stringify({ + jsonrpc: "2.0", + method: "notifications/progress", + params: { + progressToken: token, + progress: ++progressSent, + }, + })}\n\n` + ); + } + }, 250); + // Only transport cancellation or test teardown can release this POST. + return; + } + break; + } + case "ping": + result = {}; + break; + default: + throw new Error(`Unexpected MCP method: ${frame.method}`); + } + stream.write( + `event: message\ndata: ${JSON.stringify({ + jsonrpc: "2.0", + id: frame.id, + result, + })}\n\n` + ); + response.writeHead(202).end(); + } catch (error) { + errors.push(error); + response.writeHead(500).end(String(error)); + } + }); + }); + + return { + initialized, + listed, + calls, + closedStreams, + get heldPost() { + return heldPost; + }, + get progressSent() { + return progressSent; + }, + assertHealthy() { + if (errors.length) { + throw new AggregateError(errors, "Legacy SSE fixture failed"); + } + }, + async start() { + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + server.off("error", reject); + resolve(); + }); + }); + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("Legacy SSE fixture did not bind a TCP port"); + } + return `http://127.0.0.1:${address.port}/sse`; + }, + async close() { + clearInterval(heldProgressTimer); + server.closeAllConnections(); + if (server.listening) { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ); + } + }, + }; +} + +describe("Session-owned legacy SSE acknowledgement recovery", async () => { + const { env: harnessEnv, workDir } = await createSdkTestContext({ + copilotClientOptions: { connection: RuntimeConnection.forStdio() }, + }); + + it.each([ + { replaceDescriptor: false, name: "reconnects without replay and invokes the next tool" }, + { replaceDescriptor: true, name: "rejects a replacement descriptor before permission" }, + ])("$name", { timeout: 180_000 }, async ({ replaceDescriptor }) => { + execFileSync("git", ["init", "--quiet", workDir], { windowsHide: true }); + const fixture = createLegacySseServer(replaceDescriptor); + const client = new CopilotClient({ + workingDirectory: workDir, + gitHubToken: harnessEnv.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + env: { + ...harnessEnv, + TOOL_SEARCH_DISABLED: "1", + COPILOT_MCP_TOOL_CACHE: "true", + COPILOT_CACHE_HOME: join(workDir, "cache"), + COPILOT_MCP_APPS: "false", + COPILOT_DISABLE_KEYTAR: "1", + }, + }); + let starting: Promise | undefined; + onTestFinished(async () => { + const failures: unknown[] = []; + try { + failures.push(...(await client.stop())); + } catch (error) { + failures.push(error); + } + await Promise.allSettled(starting ? [starting] : []); + try { + await fixture.close(); + fixture.assertHealthy(); + } catch (error) { + failures.push(error); + } + if (failures.length) { + throw new AggregateError(failures, "Legacy SSE test cleanup failed"); + } + }); + starting = fixture.start(); + const url = await starting; + const permissions: PermissionRequest[] = []; + const events: SessionEvent[] = []; + const session = await client.createSession({ + mcpServers: { probe: { type: "sse", url, tools: ["*"], timeout: ACK_TIMEOUT_MS } }, + onPermissionRequest: (request, invocation) => { + permissions.push(request); + return approveAll(request, invocation); + }, + }); + session.on((event) => events.push(event)); + + async function waitForFixture( + condition: () => boolean, + phase: string, + timeoutMs = STEP_TIMEOUT_MS + ) { + try { + await waitForCondition( + () => { + fixture.assertHealthy(); + return condition(); + }, + { + timeoutMs, + timeoutMessage: `Legacy SSE ${phase} did not complete`, + } + ); + } catch (error) { + const servers = await session.rpc.mcp.list().then( + (listing) => listing.servers, + (listingError: unknown) => ({ error: String(listingError) }) + ); + throw new Error( + `Legacy SSE ${phase}: ${JSON.stringify({ + initialized: fixture.initialized, + listed: fixture.listed, + calls: fixture.calls, + heldPost: fixture.heldPost, + closedStreams: fixture.closedStreams, + servers, + events: events.filter((event) => event.type.startsWith("session.mcp_")), + })}`, + { cause: error } + ); + } + } + + await session.rpc.tools.initializeAndValidate(); + await waitForFixture(() => fixture.listed.includes(1), "initial listing"); + expect( + (await session.rpc.tools.getCurrentMetadata()).tools.map((tool) => tool.name) + ).toContain("probe-ping"); + // Direct SDK execution drives the real offered-tool/permission pipeline without a model. + const initial = await session.rpc.tools.execute({ + name: "probe-ping", + arguments: { marker: "held" }, + toolCallId: "sse-held", + }); + expect(initial).toMatchObject({ + resultType: "success", + textResultForLlm: expect.stringContaining("SSE_REPLY_1_held"), + }); + const permissionsBeforeReconnect = permissions.length; + // A configured 2s deadline must not silently use the old 30s or default 180s deadline. + await waitForFixture( + () => fixture.heldPost?.closed === true && fixture.closedStreams.includes(1), + "POST cancellation and SSE closure", + 10_000 + ); + expect(fixture.heldPost).toEqual({ connection: 1, closed: true, headersSent: false }); + expect(fixture.progressSent).toBeGreaterThan(0); + await waitForFixture( + () => fixture.initialized.some((connection) => connection > 1), + "automatic reconnect" + ); + await waitForCondition( + async () => + (await session.rpc.mcp.list()).servers.some( + (server) => server.name === "probe" && server.status === "connected" + ), + { + timeoutMs: STEP_TIMEOUT_MS, + timeoutMessage: "Reconnected MCP server is not connected", + } + ); + await waitForCondition( + () => + events.some( + (event) => + event.type === "session.mcp_server_removed" && + event.data.serverName === "probe" + ), + { + timeoutMs: STEP_TIMEOUT_MS, + timeoutMessage: "Session did not observe transport closure", + } + ); + + if (replaceDescriptor) { + const stale = await session.rpc.tools.execute({ + name: "probe-ping", + arguments: { marker: "stale" }, + toolCallId: "sse-stale", + }); + expect(stale).toMatchObject({ + resultType: "failure", + error: expect.stringMatching(/catalog changed|descriptor.*changed/i), + }); + expect(permissions).toHaveLength(permissionsBeforeReconnect); + expect(fixture.calls.some((call) => call.marker === "stale")).toBe(false); + await session.rpc.tools.initializeAndValidate(); + } + const subsequent = await session.rpc.tools.execute({ + name: "probe-ping", + arguments: { marker: "next" }, + toolCallId: "sse-next", + }); + expect(subsequent).toMatchObject({ + resultType: "success", + textResultForLlm: expect.stringMatching(/SSE_REPLY_[2-9]\d*_next/), + }); + if (replaceDescriptor) { + expect(permissions.slice(permissionsBeforeReconnect)).toEqual([ + expect.objectContaining({ + kind: "mcp", + serverName: "probe", + toolName: "probe-ping", + toolCallId: "sse-next", + readOnly: false, + args: { marker: "next" }, + }), + ]); + } + expect(fixture.calls.filter((call) => call.marker === "held")).toEqual([ + { connection: 1, id: fixture.calls[0].id, marker: "held" }, + ]); + expect(fixture.calls.filter((call) => call.marker === "next")).toHaveLength(1); + fixture.assertHealthy(); + }); +}); diff --git a/nodejs/test/e2e/mcp_oauth.e2e.test.ts b/nodejs/test/e2e/mcp_oauth.e2e.test.ts index ab68afc2ef..8ce68ee70f 100644 --- a/nodejs/test/e2e/mcp_oauth.e2e.test.ts +++ b/nodejs/test/e2e/mcp_oauth.e2e.test.ts @@ -63,6 +63,76 @@ describe("MCP OAuth host auth", async () => { } ); + it( + "should complete runtime-managed OAuth through a hosted callback", + { timeout: 120_000 }, + async () => { + const oauthServer = await startOAuthMcpServer(); + const serverName = "oauth-hosted-callback-mcp"; + const redirectUri = "https://agent.example.test/oauth/callback"; + const session = await client.createSession({ + onPermissionRequest: approveAll, + mcpServers: { + [serverName]: { + type: "http", + url: `${oauthServer.url}/mcp`, + tools: ["*"], + oauthClientId: "sdk-e2e-client", + oauthPublicClient: true, + } as unknown as MCPServerConfig, + }, + }); + onTestFinished(() => disconnectSession(session)); + + await waitForMcpServerStatus(session, serverName, "needs-auth"); + const login = await session.rpc.mcp.oauth.login({ + serverName, + redirectUri, + }); + + expect(login.authorizationUrl).toBeDefined(); + expect(login.authorizationId).toBeDefined(); + const authorizationUrl = new URL(login.authorizationUrl!); + expect(authorizationUrl.searchParams.get("redirect_uri")).toBe(redirectUri); + expect(authorizationUrl.searchParams.get("state")).toBe(login.authorizationId); + + const callbackUrl = new URL(redirectUri); + callbackUrl.searchParams.set("code", "accepted-code"); + callbackUrl.searchParams.set("state", login.authorizationId!); + await session.rpc.mcp.oauth.complete({ + authorizationId: login.authorizationId!, + callbackUrl: callbackUrl.toString(), + }); + + await waitForMcpServerStatus(session, serverName); + const tools = await session.rpc.mcp.listTools({ serverName }); + expect(tools.tools.map((tool) => tool.name)).toContain("whoami"); + + const requests = await oauthServer.requests(); + const tokenRequest = requests.find((request) => request.path === "/token"); + expect(tokenRequest).toBeDefined(); + const tokenForm = new URLSearchParams(tokenRequest!.body ?? ""); + expect(tokenForm.get("grant_type")).toBe("authorization_code"); + expect(tokenForm.get("code")).toBe("accepted-code"); + expect(tokenForm.get("redirect_uri")).toBe(redirectUri); + expect(tokenForm.get("client_id")).toBe("sdk-e2e-client"); + expect(tokenForm.get("code_verifier")).toEqual(expect.any(String)); + // This server issues neither a refresh token nor an account pin. + await expect( + session.rpc.mcp.oauth.authenticationStateChanged({ + serverName, + refreshSessionToken: true, + }) + ).rejects.toThrow("no refresh token or eligible saved Entra account"); + const afterRefresh = await oauthServer.requests(); + expect(afterRefresh.filter((request) => request.path === "/token")).toHaveLength(1); + expect(afterRefresh.filter((request) => request.path === "/register")).toHaveLength(0); + expect( + requests.some((request) => request.authorization === `Bearer ${EXPECTED_TOKEN}`) + ).toBe(true); + } + ); + it("should satisfy MCP OAuth using host-provided token", { timeout: 120_000 }, async () => { const oauthServer = await startOAuthMcpServer(); const serverName = "oauth-protected-mcp"; @@ -349,7 +419,9 @@ async function callWhoami( async function startOAuthMcpServer(options: { cimdSupported?: boolean } = {}): Promise<{ url: string; - requests: () => Promise>; + requests: () => Promise< + Array<{ authorization: string | null; body: string | null; path: string }> + >; }> { const child = spawn(process.execPath, [TEST_MCP_OAUTH_SERVER], { env: { diff --git a/nodejs/test/e2e/mcp_remote_session_closure.e2e.test.ts b/nodejs/test/e2e/mcp_remote_session_closure.e2e.test.ts new file mode 100644 index 0000000000..d782b86eb0 --- /dev/null +++ b/nodejs/test/e2e/mcp_remote_session_closure.e2e.test.ts @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { spawn } from "node:child_process"; +import { dirname, resolve } from "node:path"; +import { createInterface } from "node:readline"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it, onTestFinished } from "vitest"; +import type { CopilotSession, MCPServerConfig } from "../../src/index.js"; +import { approveAll } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { stopChildProcess, waitForCondition } from "./harness/sdkTestHelper.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const TEST_MCP_SESSION_EXPIRY_SERVER = resolve( + __dirname, + "../../../test/harness/test-mcp-session-expiry-server.mjs" +); + +interface McpServerStats { + initializations: number; + toolsListRequests: number; + toolCalls: number; + activeSessions: number; +} + +describe("MCP remote session closure", async () => { + const { copilotClient: client } = await createSdkTestContext({ + copilotClientOptions: { + env: { + COPILOT_MCP_APPS: "true", + MCP_APPS: "true", + }, + }, + }); + + it( + "reconnects after an idle Streamable HTTP session expires", + { timeout: 180_000 }, + async () => { + const remoteServer = await startSessionExpiryMcpServer(); + const serverName = "remote-server"; + const session = await client.createSession({ + onPermissionRequest: approveAll, + // `session.mcp.apps.callTool` below requires the negotiated + // `mcp-apps` capability, which only this opt-in requests. + enableMcpApps: true, + mcpServers: { + [serverName]: { + type: "http", + url: `${remoteServer.url}/mcp`, + tools: ["*"], + } as MCPServerConfig, + }, + }); + onTestFinished(() => disconnectSession(session)); + expect(session.capabilities.ui?.mcpApps).toBe(true); + + await waitForMcpServerStatus(session, serverName, "connected"); + expect((await remoteServer.stats()).initializations).toBe(1); + + // The server drops the session the way an idle Streamable HTTP endpoint + // expires one: every later request for it answers 404. + await remoteServer.expireSessions(); + + await waitForCondition(async () => (await remoteServer.stats()).initializations >= 2, { + timeoutMs: 120_000, + intervalMs: 250, + timeoutMessage: "remote MCP server was never re-initialized after session expiry", + }); + await waitForMcpServerStatus(session, serverName, "connected"); + + const result = await session.rpc.mcp.apps.callTool({ + serverName, + originServerName: serverName, + toolName: "remote_ping", + arguments: {}, + }); + expect(result.content).toEqual([{ type: "text", text: "remote pong" }]); + + const finalStats = await remoteServer.stats(); + expect(finalStats.toolCalls).toBe(1); + } + ); +}); + +async function waitForMcpServerStatus( + session: CopilotSession, + serverName: string, + expectedStatus: string +): Promise { + let lastStatus = ""; + await waitForCondition( + async () => { + const result = await session.rpc.mcp.list(); + const server = result.servers.find((entry) => entry.name === serverName); + lastStatus = server?.status ?? ""; + return server?.status === expectedStatus; + }, + { + timeoutMs: 120_000, + intervalMs: 200, + timeoutMessage: `${serverName} did not reach ${expectedStatus}; last status was ${lastStatus}`, + } + ); +} + +async function disconnectSession(session: CopilotSession): Promise { + try { + await session.disconnect(); + } catch { + // The session may already be gone when the test finished. + } +} + +async function startSessionExpiryMcpServer(): Promise<{ + url: string; + stats: () => Promise; + expireSessions: () => Promise; +}> { + const child = spawn(process.execPath, [TEST_MCP_SESSION_EXPIRY_SERVER], { + stdio: ["ignore", "pipe", "pipe"], + }); + onTestFinished(() => stopChildProcess(child)); + + const stderr: string[] = []; + child.stderr.on("data", (chunk) => stderr.push(String(chunk))); + + const url = await new Promise((resolvePromise, reject) => { + const rl = createInterface({ input: child.stdout }); + const timeout = setTimeout(() => { + rl.close(); + reject( + new Error(`Timed out waiting for session-expiry MCP server. ${stderr.join("")}`) + ); + }, 30_000); + + child.once("exit", (code, signal) => { + clearTimeout(timeout); + rl.close(); + reject( + new Error( + `Session-expiry MCP server exited before listening. code=${code} signal=${signal} ${stderr.join("")}` + ) + ); + }); + + rl.on("line", (line) => { + const match = /^Listening: (.+)$/.exec(line); + if (!match) { + return; + } + clearTimeout(timeout); + rl.close(); + resolvePromise(match[1]); + }); + }); + + return { + url, + stats: async () => { + const response = await fetch(`${url}/__stats`); + if (!response.ok) { + throw new Error(`Failed to read MCP server stats: ${response.status}`); + } + return (await response.json()) as McpServerStats; + }, + expireSessions: async () => { + const response = await fetch(`${url}/__expire`, { method: "POST" }); + if (!response.ok) { + throw new Error(`Failed to expire MCP sessions: ${response.status}`); + } + }, + }; +} diff --git a/nodejs/test/e2e/memory_retrieval.e2e.test.ts b/nodejs/test/e2e/memory_retrieval.e2e.test.ts new file mode 100644 index 0000000000..d1740bc83d --- /dev/null +++ b/nodejs/test/e2e/memory_retrieval.e2e.test.ts @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { mkdtempSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { approveAll } from "../../src/index.js"; +import { createSdkTestContext, DEFAULT_GITHUB_TOKEN, isCI } from "./harness/sdkTestContext.js"; + +const memoryFeatureFlags = { + "copilot-feature-agentic-memory": true, + "copilot-feature-agentic-memory-disabled": false, + copilot_feature_agentic_memory_user_scoped: true, +}; +const responseToken = "SDK_MEMORY_RETRIEVAL_COMPLETE"; +const prompt = `Reply with exactly ${responseToken}.`; +const structuredMemory = { + subject: "SDK_MEMORY_SUBJECT", + fact: "SDK_MEMORY_FACT_MARKER", + citations: ["SDK_MEMORY_CITATION"], +}; + +describe("Memory retrieval through the TypeScript SDK", async () => { + const { copilotClient, openAiEndpoint, workDir } = await createSdkTestContext({ + copilotClientOptions: { + env: { + "copilot-feature-agentic-memory": "true", + "copilot-feature-agentic-memory-disabled": "false", + copilot_feature_agentic_memory_user_scoped: "true", + }, + }, + }); + const authTokenToUse = isCI + ? DEFAULT_GITHUB_TOKEN + : (process.env.GITHUB_TOKEN ?? DEFAULT_GITHUB_TOKEN); + + async function runMemoryTurn(memoriesStatusCode = 200) { + const repositoryDirectory = mkdtempSync(join(workDir, `memory-${memoriesStatusCode}-`)); + execFileSync("git", ["init", "--initial-branch=main"], { cwd: repositoryDirectory }); + execFileSync( + "git", + [ + "remote", + "add", + "origin", + `https://github.com/octo/sdk-memory-${memoriesStatusCode}.git`, + ], + { cwd: repositoryDirectory } + ); + await openAiEndpoint.setCopilotUserByToken(authTokenToUse, { + login: "e2e-test-user", + id: 12345, + copilot_plan: "individual_pro", + is_mcp_enabled: true, + endpoints: { + api: openAiEndpoint.url, + telemetry: "https://localhost:1/telemetry", + }, + analytics_tracking_id: "e2e-test-tracking-id", + }); + await openAiEndpoint.setMemoryApiStub({ + enabled: { enabled: true }, + memories: { userMemories: [structuredMemory] }, + memoriesStatusCode, + }); + const session = await copilotClient.createSession({ + featureFlags: memoryFeatureFlags, + gitHubToken: authTokenToUse, + memory: { enabled: true }, + onPermissionRequest: approveAll, + workingDirectory: repositoryDirectory, + }); + try { + const response = await session.sendAndWait({ prompt }); + expect(response?.data.content).toBe(responseToken); + } finally { + await session.disconnect(); + } + + const requests = await openAiEndpoint.getRequests(); + const requestUrls = requests.map((request) => request.url); + const memoryRequests = requests.filter((request) => + request.url.includes("/internal/memory/v0/") + ); + const requestPaths = memoryRequests.map( + (request) => new URL(request.url, "https://memory.test").pathname + ); + const exchanges = await openAiEndpoint.getExchanges(); + const systemContent = exchanges + .flatMap((exchange) => exchange.request.messages) + .filter((message) => message.role === "system") + .map((message) => + typeof message.content === "string" + ? message.content + : JSON.stringify(message.content) + ) + .join("\n"); + const toolNames = exchanges + .flatMap((exchange) => exchange.request.tools ?? []) + .map((tool) => (tool.type === "function" ? tool.function.name : tool.type)); + + return { requestPaths, requestUrls, systemContent, toolNames }; + } + + it("injects structured memories into the model request", async () => { + const { requestPaths, requestUrls, systemContent, toolNames } = await runMemoryTurn(); + + expect( + requestPaths, + `Captured URLs: ${JSON.stringify(requestUrls)}; tools: ${JSON.stringify(toolNames)}; structured fact present: ${systemContent.includes(structuredMemory.fact)}` + ).toContain("/agents/swe/internal/memory/v0/enabled"); + expect(requestPaths).toContain("/agents/swe/internal/memory/v0/memories"); + expect(requestPaths).not.toContain("/agents/swe/internal/memory/v0/prompt"); + expect(systemContent).toContain(structuredMemory.subject); + expect(systemContent).toContain(structuredMemory.fact); + expect(systemContent).toContain(structuredMemory.citations); + }); + + it("does not inject memories when structured retrieval fails", async () => { + const { requestPaths, requestUrls, systemContent } = await runMemoryTurn(401); + + expect(requestPaths, `Captured URLs: ${JSON.stringify(requestUrls)}`).toContain( + "/agents/swe/internal/memory/v0/enabled" + ); + expect(requestPaths).toContain("/agents/swe/internal/memory/v0/memories"); + expect(requestPaths).not.toContain("/agents/swe/internal/memory/v0/prompt"); + expect(systemContent).not.toContain(structuredMemory.fact); + }); +}); diff --git a/nodejs/test/e2e/mode_handlers.e2e.test.ts b/nodejs/test/e2e/mode_handlers.e2e.test.ts index 71c4b08963..b178523cd0 100644 --- a/nodejs/test/e2e/mode_handlers.e2e.test.ts +++ b/nodejs/test/e2e/mode_handlers.e2e.test.ts @@ -2,16 +2,21 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { describe, expect, it } from "vitest"; +import { createServer } from "node:http"; +import { text } from "node:stream/consumers"; +import { describe, expect, it, onTestFinished } from "vitest"; import type { AutoModeSwitchRequest, CopilotSession, ExitPlanModeRequest, ExitPlanModeResult, + NamedProviderConfig, + ProviderModelConfig, SessionEvent, } from "../../src/index.js"; import { approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; const EVENT_TIMEOUT_MS = 30_000; const MODE_HANDLER_TOKEN = "mode-handler-token"; @@ -20,6 +25,14 @@ const PLAN_PROMPT = "Create a brief implementation plan for adding a greeting.txt file, then request approval with exit_plan_mode."; const AUTO_MODE_PROMPT = "Explain that auto mode recovered from a rate limit in one short sentence."; +const PLAN_OVERRIDE_PROMPT = + "Write a plan, then call task_complete with summary PLAN_OVERRIDE_TOOL_CALLED."; +const INTERACTIVE_AFTER_PLAN_PROMPT = "Reply with exactly INTERACTIVE_AFTER_PLAN_OVERRIDE."; +const AUTOPILOT_READY_PROMPT = "Reply with exactly AUTOPILOT_MODE_READY."; +const AUTOPILOT_TO_INTERACTIVE_PROMPT = + "Call task_complete after the test switches this in-flight turn from Autopilot to Interactive."; +const INTERACTIVE_TO_AUTOPILOT_PROMPT = + "Call task_complete after the test switches this in-flight turn from Interactive to Autopilot."; function waitForEvent( session: CopilotSession, @@ -53,7 +66,7 @@ function waitForEvent( } describe("Mode handlers", async () => { - const { copilotClient: client, openAiEndpoint, env } = await createSdkTestContext(); + const { copilotClient: client, openAiEndpoint, env, workDir } = await createSdkTestContext(); env.COPILOT_DEBUG_GITHUB_API_URL = env.COPILOT_API_URL; await openAiEndpoint.setCopilotUserByToken(MODE_HANDLER_TOKEN, { @@ -126,6 +139,404 @@ describe("Mode handlers", async () => { } }); + it("handles Plan overrides and in-flight mode changes", { timeout: 180_000 }, async () => { + const modelRequests: Array<{ + messages?: Array<{ + role?: string; + content?: unknown; + tool_calls?: Array<{ function?: { name?: string } }>; + }>; + tools?: Array<{ function?: { name?: string } }>; + }> = []; + const autopilotToInteractiveGate = Promise.withResolvers(); + const interactiveToAutopilotGate = Promise.withResolvers(); + let modelFailure: Error | undefined; + const modelServer = createServer((request, response) => { + void (async () => { + const modelRequest = JSON.parse(await text(request)) as { + messages?: Array<{ + role?: string; + content?: unknown; + tool_calls?: Array<{ function?: { name?: string } }>; + }>; + tools?: Array<{ function?: { name?: string } }>; + }; + modelRequests.push(modelRequest); + const messages = modelRequest.messages ?? []; + const latestUserIndex = messages.findLastIndex( + (message) => message.role === "user" + ); + const activeMessages = messages.slice(Math.max(0, latestUserIndex)); + const latestUserContent = JSON.stringify(messages[latestUserIndex]?.content ?? ""); + const calledTools = + activeMessages.flatMap( + (message) => + message.tool_calls + ?.map((toolCall) => toolCall.function?.name) + .filter((name): name is string => name !== undefined) ?? [] + ) ?? []; + let message; + if (latestUserContent.includes(INTERACTIVE_AFTER_PLAN_PROMPT)) { + message = { role: "assistant", content: "INTERACTIVE_AFTER_PLAN_OVERRIDE" }; + } else if (latestUserContent.includes(AUTOPILOT_READY_PROMPT)) { + if (calledTools.includes("task_complete")) { + message = { + role: "assistant", + content: "UNEXPECTED_AUTOPILOT_READY_CONTINUATION", + }; + } else { + message = { + role: "assistant", + content: null, + tool_calls: [ + { + id: "autopilot-ready-task-complete", + type: "function", + function: { + name: "task_complete", + arguments: '{"summary":"AUTOPILOT_MODE_READY"}', + }, + }, + ], + }; + } + } else if (latestUserContent.includes(AUTOPILOT_TO_INTERACTIVE_PROMPT)) { + if (calledTools.includes("task_complete")) { + message = { + role: "assistant", + content: "AUTOPILOT_TO_INTERACTIVE_CONTINUED", + }; + } else { + await autopilotToInteractiveGate.promise; + message = { + role: "assistant", + content: null, + tool_calls: [ + { + id: "autopilot-to-interactive-task-complete", + type: "function", + function: { + name: "task_complete", + arguments: '{"summary":"MODE_SWITCHED_TO_INTERACTIVE"}', + }, + }, + ], + }; + } + } else if (latestUserContent.includes(INTERACTIVE_TO_AUTOPILOT_PROMPT)) { + if (calledTools.includes("task_complete")) { + message = { + role: "assistant", + content: "UNEXPECTED_INTERACTIVE_TO_AUTOPILOT_CONTINUATION", + }; + } else { + await interactiveToAutopilotGate.promise; + message = { + role: "assistant", + content: null, + tool_calls: [ + { + id: "interactive-to-autopilot-task-complete", + type: "function", + function: { + name: "task_complete", + arguments: '{"summary":"MODE_SWITCHED_TO_AUTOPILOT"}', + }, + }, + ], + }; + } + } else if (calledTools.includes("task_complete")) { + message = { role: "assistant", content: "PLAN_OVERRIDE_CONTINUED" }; + } else { + message = { + role: "assistant", + content: null, + tool_calls: [ + { + id: "plan-override-task-complete", + type: "function", + function: { + name: "task_complete", + arguments: '{"summary":"PLAN_OVERRIDE_TOOL_CALLED"}', + }, + }, + ], + }; + } + response.writeHead(200, { "content-type": "application/json" }); + response.end( + JSON.stringify({ + id: `plan-override-${modelRequests.length}`, + object: "chat.completion", + created: 0, + model: "test-model", + choices: [ + { + index: 0, + message, + finish_reason: "tool_calls" in message ? "tool_calls" : "stop", + }, + ], + usage: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20 }, + }) + ); + })().catch((error: unknown) => { + modelFailure = error instanceof Error ? error : new Error(String(error)); + response.writeHead(500).end(); + }); + }); + let session: CopilotSession | undefined; + const sessions: CopilotSession[] = []; + let serverStart: Promise | undefined; + onTestFinished(async () => { + const errors: unknown[] = []; + for (const activeSession of sessions.toReversed()) { + try { + await activeSession.disconnect(); + } catch (error) { + errors.push(error); + } + } + if (serverStart) { + const [startup] = await Promise.allSettled([serverStart]); + if (startup.status === "rejected") { + errors.push(startup.reason); + } + } + modelServer.closeAllConnections(); + if (modelServer.listening) { + try { + await new Promise((resolve, reject) => { + modelServer.close((error) => (error ? reject(error) : resolve())); + }); + } catch (error) { + errors.push(error); + } + } + if (errors.length) { + throw new AggregateError( + errors, + "Per-message Plan override fixture cleanup failed" + ); + } + }); + serverStart = new Promise((resolve, reject) => { + modelServer.once("error", reject); + modelServer.listen(0, "127.0.0.1", resolve); + }); + await serverStart; + const address = modelServer.address(); + if (!address || typeof address === "string") { + throw new Error("Missing local model server address"); + } + + const providers: NamedProviderConfig[] = [ + { + name: "local", + type: "openai", + baseUrl: `http://127.0.0.1:${address.port}`, + apiKey: "test", + wireApi: "completions", + }, + ]; + const models: ProviderModelConfig[] = [ + { id: "model", provider: "local", modelId: "test-model", wireModel: "test-model" }, + ]; + session = await client.createSession({ + onPermissionRequest: approveAll, + workingDirectory: workDir, + providers, + models, + model: "local/model", + }); + sessions.push(session); + + const planResponse = await session.sendAndWait({ + prompt: PLAN_OVERRIDE_PROMPT, + agentMode: "plan", + }); + const interactiveResponse = await session.sendAndWait({ + prompt: INTERACTIVE_AFTER_PLAN_PROMPT, + }); + await waitForCondition(() => modelRequests.length >= 3, { + timeoutMs: EVENT_TIMEOUT_MS, + timeoutMessage: + "The local model did not receive the Plan, continuation, and Interactive requests", + }); + + if (modelFailure) { + throw modelFailure; + } + expect(modelRequests).toHaveLength(3); + const firstSystemMessages = modelRequests[0].messages?.filter( + (message) => message.role === "system" + ); + const firstUserMessages = modelRequests[0].messages?.filter( + (message) => message.role === "user" + ); + expect(JSON.stringify(firstSystemMessages)).not.toContain(""); + expect(JSON.stringify(firstUserMessages)).toContain(""); + expect(JSON.stringify(firstUserMessages)).toContain(""); + expect( + modelRequests[0].tools?.some((tool) => tool.function?.name === "task_complete") + ).toBe(true); + expect(JSON.stringify(modelRequests[1].messages)).toContain("Continue working"); + expect(JSON.stringify(modelRequests[2].messages)).toContain( + "Plan mode is no longer active." + ); + expect(planResponse?.data.content).toBe("PLAN_OVERRIDE_CONTINUED"); + expect(interactiveResponse?.data.content).toBe("INTERACTIVE_AFTER_PLAN_OVERRIDE"); + await session.disconnect(); + sessions.splice(sessions.indexOf(session), 1); + + const approvalSession = await client.createSession({ + onPermissionRequest: approveAll, + workingDirectory: workDir, + providers, + models, + model: "local/model", + }); + sessions.push(approvalSession); + const enteredAutopilot = waitForEvent( + approvalSession, + (event): event is Extract => + event.type === "session.mode_changed" && event.data.newMode === "autopilot", + "session.mode_changed event for Autopilot warm-up" + ); + await approvalSession.rpc.mode.set({ mode: "autopilot" }); + await enteredAutopilot; + let autopilotReadyState: "pending" | "idle" | "error" = "pending"; + let autopilotReadyError: Error | undefined; + const unsubscribeAutopilotReady = approvalSession.on((event) => { + if (event.type === "session.idle") { + autopilotReadyState = "idle"; + } else if (event.type === "session.error") { + autopilotReadyState = "error"; + autopilotReadyError = new Error(`${event.data.message}\n${event.data.stack ?? ""}`); + } + }); + try { + await approvalSession.send({ prompt: AUTOPILOT_READY_PROMPT }); + await waitForCondition( + () => autopilotReadyState !== "pending" || modelRequests.length >= 5, + { + timeoutMs: EVENT_TIMEOUT_MS, + timeoutMessage: "The Autopilot warm-up turn did not finish or continue", + } + ); + } finally { + unsubscribeAutopilotReady(); + } + if (autopilotReadyError) { + throw autopilotReadyError; + } + expect(autopilotReadyState).toBe("idle"); + expect(modelRequests).toHaveLength(4); + expect(JSON.stringify(modelRequests)).not.toContain( + "UNEXPECTED_AUTOPILOT_READY_CONTINUATION" + ); + + let exitAutopilotState: "pending" | "idle" | "error" = "pending"; + let exitAutopilotError: Error | undefined; + const unsubscribeExitAutopilot = approvalSession.on((event) => { + if (event.type === "session.idle") { + exitAutopilotState = "idle"; + } else if (event.type === "session.error") { + exitAutopilotState = "error"; + exitAutopilotError = new Error(`${event.data.message}\n${event.data.stack ?? ""}`); + } + }); + try { + await approvalSession.send({ prompt: AUTOPILOT_TO_INTERACTIVE_PROMPT }); + await waitForCondition(() => modelRequests.length >= 5, { + timeoutMs: EVENT_TIMEOUT_MS, + timeoutMessage: "The Autopilot request did not reach the local model", + }); + const enteredInteractive = waitForEvent( + approvalSession, + (event): event is Extract => + event.type === "session.mode_changed" && event.data.newMode === "interactive", + "session.mode_changed event for Autopilot to Interactive" + ); + await approvalSession.rpc.mode.set({ mode: "interactive" }); + await enteredInteractive; + autopilotToInteractiveGate.resolve(); + await waitForCondition( + () => exitAutopilotState !== "pending" || modelRequests.length >= 6, + { + timeoutMs: EVENT_TIMEOUT_MS, + timeoutMessage: "The Autopilot-to-Interactive turn did not finish or continue", + } + ); + if (exitAutopilotState === "pending") { + await waitForCondition(() => exitAutopilotState !== "pending", { + timeoutMs: EVENT_TIMEOUT_MS, + timeoutMessage: "The Autopilot-to-Interactive continuation did not reach idle", + }); + } + } finally { + unsubscribeExitAutopilot(); + } + if (exitAutopilotError) { + throw exitAutopilotError; + } + expect(exitAutopilotState).toBe("idle"); + expect(modelRequests.length).toBeGreaterThanOrEqual(6); + expect( + modelRequests + .slice(5) + .some((request) => JSON.stringify(request.messages).includes("Continue working")) + ).toBe(true); + + const beforeEnterAutopilot = modelRequests.length; + let enterAutopilotState: "pending" | "idle" | "error" = "pending"; + let enterAutopilotError: Error | undefined; + const unsubscribeEnterAutopilot = approvalSession.on((event) => { + if (event.type === "session.idle") { + enterAutopilotState = "idle"; + } else if (event.type === "session.error") { + enterAutopilotState = "error"; + enterAutopilotError = new Error(`${event.data.message}\n${event.data.stack ?? ""}`); + } + }); + try { + await approvalSession.send({ prompt: INTERACTIVE_TO_AUTOPILOT_PROMPT }); + await waitForCondition(() => modelRequests.length >= beforeEnterAutopilot + 1, { + timeoutMs: EVENT_TIMEOUT_MS, + timeoutMessage: "The Interactive request did not reach the local model", + }); + const reenteredAutopilot = waitForEvent( + approvalSession, + (event): event is Extract => + event.type === "session.mode_changed" && event.data.newMode === "autopilot", + "session.mode_changed event for Interactive to Autopilot" + ); + await approvalSession.rpc.mode.set({ mode: "autopilot" }); + await reenteredAutopilot; + interactiveToAutopilotGate.resolve(); + await waitForCondition( + () => + enterAutopilotState !== "pending" || + modelRequests.length >= beforeEnterAutopilot + 2, + { + timeoutMs: EVENT_TIMEOUT_MS, + timeoutMessage: "The Interactive-to-Autopilot turn did not finish or continue", + } + ); + } finally { + unsubscribeEnterAutopilot(); + } + if (enterAutopilotError) { + throw enterAutopilotError; + } + expect(enterAutopilotState).toBe("idle"); + expect(modelRequests).toHaveLength(beforeEnterAutopilot + 1); + expect(JSON.stringify(modelRequests)).not.toContain( + "UNEXPECTED_INTERACTIVE_TO_AUTOPILOT_CONTINUATION" + ); + }); + it("should invoke auto mode switch handler when rate limited", async () => { const autoModeSwitchRequests: AutoModeSwitchRequest[] = []; let session: CopilotSession | undefined; diff --git a/nodejs/test/e2e/model_effort_resume.e2e.test.ts b/nodejs/test/e2e/model_effort_resume.e2e.test.ts new file mode 100644 index 0000000000..d458118761 --- /dev/null +++ b/nodejs/test/e2e/model_effort_resume.e2e.test.ts @@ -0,0 +1,221 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { describe, expect, it, onTestFinished } from "vitest"; +import { approveAll, RuntimeConnection } from "../../src/index.js"; +import type { NamedProviderConfig, ProviderModelConfig } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +describe("Model-bound effort across runtime restart", async () => { + const { createClient, openAiEndpoint, env } = await createSdkTestContext(); + + it.each([ + { override: undefined, replacement: "switch", plan: false }, + { override: undefined, replacement: "policy", plan: false }, + { override: "rpc", replacement: "policy", plan: false }, + { override: "options", replacement: "switch", plan: false }, + { override: "options", replacement: "options", plan: false }, + { override: "combined", replacement: "switch", plan: false }, + { override: "model-only", replacement: "options", plan: false }, + { override: undefined, replacement: "switch", plan: true }, + { override: "rpc", replacement: "switch", plan: true }, + ] as const)( + "restores ownership with $override effort override, $replacement replacement, and plan=$plan", + async ({ override, replacement, plan }) => { + const providers: NamedProviderConfig[] = [ + { + name: "effort-resume", + type: "openai", + wireApi: "completions", + baseUrl: openAiEndpoint.url, + }, + ]; + const models: ProviderModelConfig[] = [ + { id: "bound", provider: "effort-resume", modelId: "gpt-5.4-mini" }, + { id: "replacement", provider: "effort-resume", modelId: "gpt-5.4-mini" }, + ]; + const customAgents = [ + { + name: "cold-resume-agent", + prompt: "Respond concisely.", + model: "effort-resume/bound:defaultReasoningEffort=high", + }, + ]; + // Pin child-process transport: recreating an in-process client is not a cold restart. + const originalClient = createClient({ connection: RuntimeConnection.forStdio() }); + onTestFinished(async () => { + expect(await originalClient.stop()).toHaveLength(0); + }); + const fixtureSession = await originalClient.createSession({ + onPermissionRequest: approveAll, + model: "effort-resume/bound", + providers, + models, + customAgents, + }); + // Create workspace metadata without a model turn; this does not enable journal persistence. + await fixtureSession.rpc.name.set({ + name: `effort-resume-${fixtureSession.sessionId}`, + }); + await fixtureSession.rpc.agent.select({ name: "cold-resume-agent" }); + await fixtureSession.rpc.model.switchTo({ + modelId: "effort-resume/bound", + reasoningEffort: "high", + source: "agent", + }); + const fixtureEvents = await fixtureSession.getEvents(); + expect(fixtureEvents[0]?.type).toBe("session.start"); + const transported = fixtureEvents + .filter((event) => event.type === "session.model_change") + .at(-1); + expect(transported?.data.reasoningEffort).toBe("high"); + expect(transported?.data.source).toBe("sdk"); + expect(transported?.data.reasoningEffortModel).toBeUndefined(); + await originalClient.rpc.sessions.save({ sessionId: fixtureSession.sessionId }); + await originalClient.rpc.sessions.close({ sessionId: fixtureSession.sessionId }); + expect(await originalClient.stop()).toHaveLength(0); + + // SDK callers cannot claim agent provenance, and a no-turn session has no journal. + // Seed its emitted history and agent receipt only after the writer stops. + const eventsPath = join( + env.COPILOT_HOME, + "session-state", + fixtureSession.sessionId, + "events.jsonl" + ); + const authored = fixtureEvents + .filter((event) => event.type === "session.model_change") + .at(-1); + if (!authored) { + throw new Error("The runtime did not emit the model switch fixture"); + } + expect(authored.data.reasoningEffort).toBe("high"); + authored.data.source = "agent"; + authored.data.reasoningEffortModel = "effort-resume/bound"; + await writeFile( + eventsPath, + `${fixtureEvents.map((event) => JSON.stringify(event)).join("\n")}\n` + ); + + const boundClient = createClient({ connection: RuntimeConnection.forStdio() }); + onTestFinished(async () => { + expect(await boundClient.stop()).toHaveLength(0); + }); + const session = await boundClient.resumeSession(fixtureSession.sessionId, { + onPermissionRequest: approveAll, + providers, + models, + customAgents, + }); + const bound = (await session.getEvents()) + .filter((event) => event.type === "session.model_change") + .at(-1); + expect(bound?.data.reasoningEffortModel).toBe("effort-resume/bound"); + if (override === "rpc") { + await session.rpc.model.setReasoningEffort({ reasoningEffort: "high" }); + } else if (override === "options") { + await session.rpc.options.update({ reasoningEffort: "high" }); + } else if (override === "combined") { + await session.rpc.options.update({ + model: "effort-resume/replacement", + reasoningEffort: "high", + }); + } else if (override === "model-only") { + await session.rpc.options.update({ model: "effort-resume/bound" }); + expect((await session.rpc.model.getCurrent()).reasoningEffort).toBe("high"); + const unchanged = (await session.getEvents()) + .filter((event) => event.type === "session.model_change") + .at(-1); + expect(unchanged?.data.reasoningEffortModel).toBe("effort-resume/bound"); + await session.rpc.options.update({ model: "effort-resume/replacement" }); + } + const updatedModel = + override === "combined" || override === "model-only" + ? "effort-resume/replacement" + : "effort-resume/bound"; + const expectedEffort = override === "model-only" ? undefined : "high"; + const updatedControls = await session.rpc.model.getCurrent(); + expect(updatedControls.modelId).toBe(updatedModel); + expect(updatedControls.reasoningEffort).toBe(expectedEffort); + const beforeRestart = (await session.getEvents()) + .filter((event) => event.type === "session.model_change") + .at(-1); + expect(beforeRestart?.data.reasoningEffort).toBe(expectedEffort ?? null); + expect(beforeRestart?.data.newModel).toBe(updatedModel); + expect(beforeRestart?.data.reasoningEffortModel).toBe( + override === undefined ? "effort-resume/bound" : undefined + ); + const planSelection = { + planModelConfigured: true, + planModel: "effort-resume/replacement", + planReasoningEffort: "low", + }; + if (plan) { + await session.rpc.mode.set({ mode: "plan", ...planSelection }); + expect(await session.rpc.mode.get()).toBe("plan"); + expect(await session.rpc.model.getCurrent()).toMatchObject({ + modelId: "effort-resume/replacement", + reasoningEffort: "low", + }); + } + await boundClient.rpc.sessions.save({ sessionId: session.sessionId }); + await boundClient.rpc.sessions.close({ sessionId: session.sessionId }); + expect(await boundClient.stop()).toHaveLength(0); + + const resumedClient = createClient({ connection: RuntimeConnection.forStdio() }); + onTestFinished(async () => { + expect(await resumedClient.stop()).toHaveLength(0); + }); + const resumed = await resumedClient.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + providers, + models, + customAgents, + }); + if (plan) { + expect(await resumed.rpc.mode.get()).toBe("plan"); + await resumed.rpc.mode.set({ mode: "interactive", ...planSelection }); + expect(await resumed.rpc.mode.get()).toBe("interactive"); + } + const restoredControls = await resumed.rpc.model.getCurrent(); + expect(restoredControls.modelId).toBe(updatedModel); + expect(restoredControls.reasoningEffort).toBe(expectedEffort); + if (override === "model-only") { + await resumed.rpc.options.update({ model: "effort-resume/bound" }); + const revertedControls = await resumed.rpc.model.getCurrent(); + expect(revertedControls.modelId).toBe("effort-resume/bound"); + expect(revertedControls.reasoningEffort).toBeUndefined(); + } + if (replacement === "policy") { + await resumed.rpc.model.setAllowedModels({ + allowedModels: ["effort-resume/replacement"], + }); + } else if (replacement === "options") { + await resumed.rpc.options.update({ model: "effort-resume/replacement" }); + } else { + await resumed.rpc.model.switchTo({ modelId: "effort-resume/replacement" }); + } + const replacementEffort = + override === undefined || override === "model-only" ? undefined : "high"; + const replacedControls = await resumed.rpc.model.getCurrent(); + expect(replacedControls.modelId).toBe("effort-resume/replacement"); + expect(replacedControls.reasoningEffort).toBe(replacementEffort); + const replaced = (await resumed.getEvents()) + .filter((event) => event.type === "session.model_change") + .at(-1); + expect(replaced?.data.newModel).toBe("effort-resume/replacement"); + expect(replaced?.data.reasoningEffort).toBe(replacementEffort ?? null); + expect(replaced?.data.reasoningEffortModel).toBeUndefined(); + await resumedClient.rpc.sessions.close({ sessionId: resumed.sessionId }); + expect(await resumedClient.stop()).toHaveLength(0); + expect( + (await openAiEndpoint.getRequests()).filter((request) => + /\/(?:completions|responses|messages)(?:\?|$)/.test(request.url) + ) + ).toHaveLength(0); + } + ); +}); diff --git a/nodejs/test/e2e/multi-client.e2e.test.ts b/nodejs/test/e2e/multi-client.e2e.test.ts index a44ceec3c3..f0f7a6c33e 100644 --- a/nodejs/test/e2e/multi-client.e2e.test.ts +++ b/nodejs/test/e2e/multi-client.e2e.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it, afterAll } from "vitest"; import { z } from "zod"; -import { CopilotClient, defineTool, approveAll, RuntimeConnection } from "../../src/index.js"; +import { defineTool, approveAll, RuntimeConnection } from "../../src/index.js"; import type { SessionEvent } from "../../src/index.js"; import { createSdkTestContext, isInProcessTransport } from "./harness/sdkTestContext"; @@ -24,7 +24,7 @@ describe("Multi-client broadcast", async () => { await initSession.disconnect(); const runtimePort = (client1 as unknown as { runtimePort: number }).runtimePort; - let client2 = new CopilotClient({ + let client2 = ctx.createClient({ connection: RuntimeConnection.forUri(`localhost:${runtimePort}`, { connectionToken: tcpConnectionToken, }), @@ -360,7 +360,7 @@ describe("Multi-client broadcast", async () => { process.removeListener("unhandledRejection", suppressDisposed); // Recreate client2 for cleanup in afterAll (but don't rejoin the session) - client2 = new CopilotClient({ + client2 = ctx.createClient({ connection: RuntimeConnection.forUri(`localhost:${runtimePort}`, { connectionToken: tcpConnectionToken, }), @@ -375,4 +375,52 @@ describe("Multi-client broadcast", async () => { expect(afterResponse?.data.content).not.toContain("EPHEMERAL_"); } ); + + it( + "clearing one client's tools keeps the other client's tools", + { timeout: 90_000 }, + async () => { + const keptTool = defineTool("kept_tool", { + description: "A tool whose client keeps supplying it", + parameters: z.object({ input: z.string() }), + handler: ({ input }) => `KEPT_${input}`, + }); + const clearedTool = defineTool("cleared_tool", { + description: "A tool whose client stops supplying it", + parameters: z.object({ input: z.string() }), + handler: ({ input }) => `CLEARED_${input}`, + }); + + // Client 1 creates a session with kept_tool, and client 2 joins it with cleared_tool. + const session1 = await client1.createSession({ + onPermissionRequest: approveAll, + tools: [keptTool], + }); + const session2 = await client2.resumeSession(session1.sessionId, { + onPermissionRequest: approveAll, + tools: [clearedTool], + }); + + // Client 2 removes all of its tools. + await session2.setTools([]); + + const response = await session1.sendAndWait({ + prompt: "Use the kept_tool with input 'test' and tell me the result.", + }); + expect(response?.data.content).toContain("KEPT_test"); + + // Every model request still offers client 1's tool, and none offers client 2's. + const exchanges = await ctx.openAiEndpoint.getExchanges(); + expect(exchanges.length).toBeGreaterThan(0); + for (const exchange of exchanges) { + const tools = (exchange.request.tools ?? []).map((tool) => + "function" in tool ? tool.function.name : "" + ); + expect(tools).toContain("kept_tool"); + expect(tools).not.toContain("cleared_tool"); + } + + await session2.disconnect(); + } + ); }); diff --git a/nodejs/test/e2e/pending_work_resume.e2e.test.ts b/nodejs/test/e2e/pending_work_resume.e2e.test.ts index f2bb16ca6d..65ab9fccc9 100644 --- a/nodejs/test/e2e/pending_work_resume.e2e.test.ts +++ b/nodejs/test/e2e/pending_work_resume.e2e.test.ts @@ -3,6 +3,9 @@ *--------------------------------------------------------------------------------------------*/ import { describe, expect, it, onTestFinished } from "vitest"; +import { existsSync, readFileSync } from "fs"; +import { join } from "path"; +import { fileURLToPath } from "url"; import { z } from "zod"; import { approveAll, CopilotClient, defineTool, RuntimeConnection } from "../../src/index.js"; import type { @@ -11,6 +14,7 @@ import type { PermissionRequest, PermissionRequestedEvent, PermissionRequestResult, + SessionEvent, } from "../../src/index.js"; import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js"; import { waitForCondition } from "./harness/sdkTestHelper.js"; @@ -136,11 +140,11 @@ function waitForPermissionRequest(session: CopilotSession): Promise { - const { env, workDir } = await createSdkTestContext(); + const { env, workDir, openAiEndpoint, createClient } = await createSdkTestContext(); const SHARED_TOKEN = "pending-work-resume-shared-test-token"; function createTcpServer(): CopilotClient { - const server = new CopilotClient({ + const server = createClient({ workingDirectory: workDir, env, gitHubToken: DEFAULT_GITHUB_TOKEN, @@ -160,7 +164,7 @@ describe("Pending work resume", async () => { } function createConnectingClient(cliUrl: string): CopilotClient { - const client = new CopilotClient({ + const client = createClient({ connection: RuntimeConnection.forUri(cliUrl, { connectionToken: SHARED_TOKEN }), }); onTestFinished(async () => { @@ -181,6 +185,52 @@ describe("Pending work resume", async () => { return `localhost:${port}`; } + async function forceStopAfterSessionLockObserved( + suspendedClient: CopilotClient, + sessionId: string + ): Promise { + const lockObserver = new CopilotClient({ + workingDirectory: workDir, + env, + gitHubToken: DEFAULT_GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ + path: process.env.COPILOT_CLI_PATH, + }), + }); + try { + await lockObserver.start(); + await waitForCondition( + async () => { + const result = await lockObserver.rpc.sessions.checkInUse({ + sessionIds: [sessionId], + }); + return result.inUse.includes(sessionId); + }, + { + timeoutMs: PENDING_WORK_TIMEOUT_MS, + timeoutMessage: `Timed out waiting for session '${sessionId}' to acquire its lock.`, + } + ); + + await suspendedClient.forceStop(); + + await waitForCondition( + async () => { + const result = await lockObserver.rpc.sessions.checkInUse({ + sessionIds: [sessionId], + }); + return !result.inUse.includes(sessionId); + }, + { + timeoutMs: PENDING_WORK_TIMEOUT_MS, + timeoutMessage: `Timed out waiting for session '${sessionId}' to release its lock.`, + } + ); + } finally { + await lockObserver.forceStop(); + } + } + it( "should continue pending permission request after resume", { timeout: TEST_TIMEOUT_MS }, @@ -223,7 +273,7 @@ describe("Pending work resume", async () => { await permissionRequestedP; expect(initialRequest.kind).toBe("custom-tool"); - await suspendedClient.forceStop(); + await forceStopAfterSessionLockObserved(suspendedClient, sessionId); const resumedTcpClient = createConnectingClient(cliUrl); const session2 = await resumedTcpClient.resumeSession(sessionId, { @@ -301,7 +351,7 @@ describe("Pending work resume", async () => { ) ).toBe("beta"); - await suspendedClient.forceStop(); + await forceStopAfterSessionLockObserved(suspendedClient, sessionId); const resumedClient = createConnectingClient(cliUrl); const session2 = await resumedClient.resumeSession(sessionId, { @@ -380,7 +430,7 @@ describe("Pending work resume", async () => { expect(await originalToolAStarted.promise).toBe("alpha"); expect(await originalToolBStarted.promise).toBe("beta"); - await suspendedClient.forceStop(); + await forceStopAfterSessionLockObserved(suspendedClient, sessionId); const resumedClient = createConnectingClient(cliUrl); const session2 = await resumedClient.resumeSession(sessionId, { @@ -394,7 +444,21 @@ describe("Pending work resume", async () => { requestId: toolB.data.requestId, result: "PARALLEL_B_BETA", }); - expect(resultB.success).toBe(true); + expect( + resultB.success, + resultB.success + ? undefined + : `Rejected request ${toolB.data.requestId}; resumed events: ${JSON.stringify( + ( + await session2.rpc.eventLog.read({ max: 100, waitMs: 0 }) + ).events.filter( + (event) => + event.type.startsWith("external_tool.") || + event.type === "tool.execution_complete" || + event.type === "session.resume" + ) + )}` + ).toBe(true); const resultA = await session2.rpc.tools.handlePendingToolCall({ requestId: toolA.data.requestId, result: "PARALLEL_A_ALPHA", @@ -413,6 +477,241 @@ describe("Pending work resume", async () => { } ); + it( + "should preserve a completed sibling's result on cold resume", + { timeout: TEST_TIMEOUT_MS }, + async () => { + const toolBStarted = deferred(); + const releaseToolB = deferred(); + let toolACalls = 0; + + await openAiEndpoint.updateConfig({ + filePath: fileURLToPath( + new URL( + "../../../test/snapshots/pending_work_resume/should_preserve_a_completed_siblings_result_on_cold_resume.yaml", + import.meta.url + ) + ), + workDir, + }); + const server = createTcpServer(); + await server.start(); + const cliUrl = getCliUrl(server); + + const originalClient = createConnectingClient(cliUrl); + const tools = [ + defineTool("pending_lookup_a", { + description: "Looks up the first value after resumption", + parameters: z.object({ value: z.string() }), + handler: ({ value }) => { + toolACalls++; + return `PARALLEL_A_${value.toUpperCase()}`; + }, + }), + defineTool("pending_lookup_b", { + description: "Looks up the second value after resumption", + parameters: z.object({ value: z.string() }), + handler: async ({ value }) => { + toolBStarted.resolve(value); + return await releaseToolB.promise; + }, + }), + ]; + const session1 = await originalClient.createSession({ + tools, + onPermissionRequest: approveAll, + }); + const sessionId = session1.sessionId; + const toolRequestsP = waitForExternalToolRequests(session1, [ + "pending_lookup_a", + "pending_lookup_b", + ]); + + await session1.send({ + prompt: "Call pending_lookup_a with value 'alpha' and pending_lookup_b with value 'beta', then reply with both results.", + }); + + const requests = await toolRequestsP; + expect( + await waitWithTimeout(toolBStarted.promise, PENDING_WORK_TIMEOUT_MS, "toolBStarted") + ).toBe("beta"); + const toolA = requests["pending_lookup_a"]; + const toolB = requests["pending_lookup_b"]; + await waitForCondition( + async () => + (await session1.getEvents()).some( + (event) => + event.type === "external_tool.completed" && + event.data.requestId === toolA.data.requestId + ), + { + timeoutMs: PENDING_WORK_TIMEOUT_MS, + timeoutMessage: "Timed out waiting for tool A's external completion", + } + ); + await waitForCondition( + async () => + (await session1.getEvents()).some( + (event) => + event.type === "tool.execution_complete" && + event.data.toolCallId === toolA.data.toolCallId + ), + { + timeoutMs: PENDING_WORK_TIMEOUT_MS, + timeoutMessage: "Tool A completed, but its result was not persisted before B", + } + ); + + const beforeResume = await session1.getEvents(); + expect(toolACalls).toBe(1); + expect( + beforeResume.find( + (event) => + event.type === "tool.execution_complete" && + event.data.toolCallId === toolA.data.toolCallId + ) + ).toMatchObject({ + data: { success: true, result: { content: "PARALLEL_A_ALPHA" } }, + }); + expect( + beforeResume + .filter( + ( + event + ): event is Extract => + event.type === "tool.execution_complete" && + [toolA.data.toolCallId, toolB.data.toolCallId].includes( + event.data.toolCallId + ) + ) + .map((event) => event.data.toolCallId) + ).toEqual([toolA.data.toolCallId]); + + const eventsPath = join(env.COPILOT_HOME, "session-state", sessionId, "events.jsonl"); + const readPersistedEvents = (): SessionEvent[] => { + if (!existsSync(eventsPath)) return []; + const content = readFileSync(eventsPath, "utf8"); + const lastCompleteLine = content.lastIndexOf("\n"); + if (lastCompleteLine < 0) return []; + return content + .slice(0, lastCompleteLine) + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line) as SessionEvent); + }; + await waitForCondition( + () => + readPersistedEvents().some( + (event) => + event.type === "tool.execution_complete" && + event.data.toolCallId === toolA.data.toolCallId && + event.data.success && + event.data.result?.content === "PARALLEL_A_ALPHA" + ), + { + timeoutMs: PENDING_WORK_TIMEOUT_MS, + timeoutMessage: "Tool A's result was not flushed to events.jsonl before B", + } + ); + expect( + readPersistedEvents().filter( + (event) => + event.type === "tool.execution_complete" && + event.data.toolCallId === toolB.data.toolCallId + ) + ).toHaveLength(0); + + const lockObserver = new CopilotClient({ + workingDirectory: workDir, + env, + gitHubToken: DEFAULT_GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + }); + try { + await lockObserver.start(); + await waitForCondition( + async () => + ( + await lockObserver.rpc.sessions.checkInUse({ + sessionIds: [sessionId], + }) + ).inUse.includes(sessionId), + { + timeoutMs: PENDING_WORK_TIMEOUT_MS, + timeoutMessage: `Timed out waiting for session '${sessionId}' to acquire its lock.`, + } + ); + await server.forceStop(); + await originalClient.forceStop(); + await waitForCondition( + async () => + !( + await lockObserver.rpc.sessions.checkInUse({ + sessionIds: [sessionId], + }) + ).inUse.includes(sessionId), + { + timeoutMs: PENDING_WORK_TIMEOUT_MS, + timeoutMessage: `Timed out waiting for session '${sessionId}' to release its lock.`, + } + ); + } finally { + await lockObserver.forceStop(); + } + + const restartedServer = createTcpServer(); + await restartedServer.start(); + const resumedClient = createConnectingClient(getCliUrl(restartedServer)); + const session2 = await resumedClient.resumeSession(sessionId, { + continuePendingWork: false, + tools, + onPermissionRequest: approveAll, + }); + const events = await session2.getEvents(); + expect(events.find((event) => event.type === "session.resume")).toMatchObject({ + data: { continuePendingWork: false, sessionWasActive: false }, + }); + expect( + events.filter( + (event) => + event.type === "external_tool.completed" && + event.data.requestId === toolA.data.requestId + ) + ).toHaveLength(1); + expect( + events.filter( + (event) => + event.type === "tool.execution_complete" && + event.data.toolCallId === toolA.data.toolCallId + ) + ).toHaveLength(1); + expect( + events.filter( + (event) => + event.type === "tool.execution_complete" && + event.data.toolCallId === toolB.data.toolCallId + ) + ).toHaveLength(0); + for (const request of [toolA, toolB]) { + expect( + ( + await session2.rpc.tools.handlePendingToolCall({ + requestId: request.data.requestId, + result: "LATE_RESULT", + }) + ).success + ).toBe(false); + } + expect(toolACalls).toBe(1); + const followUp = await session2.sendAndWait({ + prompt: "Use the completed lookup result and report it without re-running pending_lookup_a. Reply with exactly: COLD_RESUMED_A_RETAINED", + }); + expect(followUp?.data.content).toContain("COLD_RESUMED_A_RETAINED"); + expect(toolACalls).toBe(1); + await session2.disconnect(); + } + ); + it( "should resume successfully when no pending work exists", { timeout: TEST_TIMEOUT_MS }, @@ -545,46 +844,7 @@ describe("Pending work resume", async () => { } if (scenario.disconnectOriginalClient) { - const lockObserver = new CopilotClient({ - workingDirectory: workDir, - env, - gitHubToken: DEFAULT_GITHUB_TOKEN, - connection: RuntimeConnection.forStdio({ - path: process.env.COPILOT_CLI_PATH, - }), - }); - try { - await lockObserver.start(); - await waitForCondition( - async () => { - const result = await lockObserver.rpc.sessions.checkInUse({ - sessionIds: [sessionId], - }); - return result.inUse.includes(sessionId); - }, - { - timeoutMs: PENDING_WORK_TIMEOUT_MS, - timeoutMessage: `Timed out waiting for session '${sessionId}' to acquire its lock.`, - } - ); - - await suspendedClient.forceStop(); - - await waitForCondition( - async () => { - const result = await lockObserver.rpc.sessions.checkInUse({ - sessionIds: [sessionId], - }); - return !result.inUse.includes(sessionId); - }, - { - timeoutMs: PENDING_WORK_TIMEOUT_MS, - timeoutMessage: `Timed out waiting for session '${sessionId}' to release its lock.`, - } - ); - } finally { - await lockObserver.forceStop(); - } + await forceStopAfterSessionLockObserved(suspendedClient, sessionId); } const resumedClient = createConnectingClient(cliUrl); diff --git a/nodejs/test/e2e/permissions.e2e.test.ts b/nodejs/test/e2e/permissions.e2e.test.ts index 565719e0a4..e9ed80d5d0 100644 --- a/nodejs/test/e2e/permissions.e2e.test.ts +++ b/nodejs/test/e2e/permissions.e2e.test.ts @@ -5,7 +5,7 @@ import { realpathSync } from "fs"; import { mkdir, readFile, writeFile } from "fs/promises"; import { join } from "path"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it, onTestFailed, vi } from "vitest"; import { z } from "zod"; import type { PermissionDecisionContext, @@ -237,13 +237,19 @@ describe("Permission callbacks", async () => { it("should resume session with permission handler", async () => { const permissionRequests: PermissionRequest[] = []; + let phase = "creating the initial session"; + onTestFailed(() => { + console.error(`Permission resume test failed while ${phase}`); + }); // Create initial session const session1 = await client.createSession({ onPermissionRequest: approveAll }); const sessionId = session1.sessionId; + phase = "sending the initial prompt"; await session1.sendAndWait({ prompt: "What is 1+1?" }); // Resume with permission handler + phase = "resuming the session"; const session2 = await client.resumeSession(sessionId, { onPermissionRequest: (request) => { permissionRequests.push(request); @@ -251,13 +257,16 @@ describe("Permission callbacks", async () => { }, }); + phase = "sending the resumed prompt"; await session2.sendAndWait({ prompt: "Run 'echo resumed' for me", }); // Should have permission requests from resumed session + phase = "verifying permission requests"; expect(permissionRequests.length).toBeGreaterThan(0); + phase = "disconnecting the resumed session"; await session2.disconnect(); }); diff --git a/nodejs/test/e2e/rewind.e2e.test.ts b/nodejs/test/e2e/rewind.e2e.test.ts index 7fdfee94c8..25e8199bf1 100644 --- a/nodejs/test/e2e/rewind.e2e.test.ts +++ b/nodejs/test/e2e/rewind.e2e.test.ts @@ -7,6 +7,7 @@ import { join, resolve } from "node:path"; import { describe, expect, it } from "vitest"; import { approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { retry } from "./harness/sdkTestHelper.js"; const FILE_NAME = "rewind-sdk.txt"; const ORIGINAL_FILE_CONTENT = "Original rewind content"; @@ -36,6 +37,18 @@ describe("Rewind", async () => { }); try { + await retry( + "initialize rewind file-change tracking", + async () => { + const initial = await session.rpc.history.listRewindPoints(); + expect(initial.unavailableReason).toBeUndefined(); + expect(initial.fileChangeTrackingEnabled).toBe(true); + expect(initial.points).toHaveLength(0); + }, + 300, + 100 + ); + const ready = await session.sendAndWait({ prompt: `Use the edit tool to replace the exact contents of ${FILE_NAME} from ${ORIGINAL_FILE_CONTENT} to ${PREPARED_FILE_CONTENT}. After the tool succeeds, reply with exactly SDK_REWIND_READY.`, }); diff --git a/nodejs/test/e2e/rpc_additional_edge_cases.e2e.test.ts b/nodejs/test/e2e/rpc_additional_edge_cases.e2e.test.ts new file mode 100644 index 0000000000..d31aa61ced --- /dev/null +++ b/nodejs/test/e2e/rpc_additional_edge_cases.e2e.test.ts @@ -0,0 +1,127 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { randomUUID } from "node:crypto"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { approveAll } from "../../src/index.js"; +import type { CopilotSession } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; + +describe("RPC boundary semantics", async () => { + const { copilotClient: client, workDir } = await createSdkTestContext(); + + async function withSession(action: (session: CopilotSession) => Promise): Promise { + const session = await client.createSession({ onPermissionRequest: approveAll }); + try { + await action(session); + } finally { + await session.disconnect(); + } + } + + it("shell exec with zero timeout does not kill a running command", async () => { + await withSession(async (session) => { + const marker = join(workDir, `zero-timeout-${randomUUID()}.txt`); + const command = + process.platform === "win32" + ? `ping 127.0.0.1 -n 2 >nul & echo alive>"${marker}" & ping 127.0.0.1 -n 61 >nul` + : `sleep 1; printf alive > '${marker}'; sleep 60`; + const result = await session.rpc.shell.exec({ command, cwd: workDir, timeout: 0 }); + expect(result.processId).toBeTruthy(); + let observedRunning = false; + try { + await waitForCondition(() => existsSync(marker), { + timeoutMessage: `Zero-timeout shell command did not write ${marker}`, + }); + observedRunning = true; + } finally { + const killed = await session.rpc.shell.kill({ processId: result.processId }); + if (observedRunning) { + expect(killed.killed).toBe(true); + } + } + }); + }, 60_000); + + it("workspace files round-trip empty, Unicode with NUL, and large content", async () => { + await withSession(async (session) => { + const files = new Map([ + [`empty-${randomUUID()}.txt`, ""], + [`unicode-${randomUUID()}.txt`, "Hello, 世界! 🚀✨ Привет\u0000end"], + [ + `large-${randomUUID()}.txt`, + "abcdefghijklmnopqrstuvwxyz".repeat(10_083).slice(0, 256 * 1024), + ], + ]); + for (const [path, content] of files) { + await session.rpc.workspaces.createFile({ path, content }); + expect((await session.rpc.workspaces.readFile({ path })).content).toBe(content); + } + const listed = (await session.rpc.workspaces.listFiles()).files; + expect(listed).toEqual(expect.arrayContaining([...files.keys()])); + }); + }); + + it("workspace listing retains every created file and getWorkspace stays stable", async () => { + await withSession(async (session) => { + const paths = Array.from({ length: 5 }, (_, i) => `order-${randomUUID()}-${i}.txt`); + for (const path of paths) { + await session.rpc.workspaces.createFile({ path, content: `content-${path}` }); + } + for (let i = 0; i < 2; i++) { + expect((await session.rpc.workspaces.listFiles()).files).toEqual( + expect.arrayContaining(paths) + ); + } + const first = await session.rpc.workspaces.getWorkspace(); + expect(await session.rpc.workspaces.getWorkspace()).toEqual(first); + }); + }); + + it("empty plan update and repeated delete preserve empty plan state", async () => { + await withSession(async (session) => { + await session.rpc.plan.update({ content: "" }); + expect((await session.rpc.plan.read()).content).toBe(""); + await session.rpc.plan.delete(); + await session.rpc.plan.delete(); + expect(await session.rpc.plan.read()).toMatchObject({ + exists: false, + content: null, + }); + }); + }); + + it("repeated mode, Unicode name, and permission toggles round-trip", async () => { + await withSession(async (session) => { + for (let i = 0; i < 3; i++) { + await session.rpc.mode.set({ mode: "plan" }); + } + expect(await session.rpc.mode.get()).toBe("plan"); + + const name = "セッション 名前 ☕ – test"; + await session.rpc.name.set({ name }); + expect((await session.rpc.name.get()).name).toBe(name); + + expect((await session.rpc.permissions.resetSessionApprovals()).success).toBe(true); + for (const enabled of [true, true, false, false]) { + expect((await session.rpc.permissions.setApproveAll({ enabled })).success).toBe( + true + ); + } + }); + }); + + it("fresh session metrics have zero tokens and a populated start time", async () => { + await withSession(async (session) => { + const metrics = await session.rpc.usage.getMetrics(); + expect(metrics.lastCallInputTokens).toBe(0); + expect(metrics.lastCallOutputTokens).toBe(0); + expect(metrics.totalUserRequests).toBe(0); + expect(Number.isNaN(Date.parse(metrics.sessionStartTime))).toBe(false); + }); + }); +}); diff --git a/nodejs/test/e2e/rpc_extensions_loaded.e2e.test.ts b/nodejs/test/e2e/rpc_extensions_loaded.e2e.test.ts new file mode 100644 index 0000000000..c2dd9f3931 --- /dev/null +++ b/nodejs/test/e2e/rpc_extensions_loaded.e2e.test.ts @@ -0,0 +1,233 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { execFileSync } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import { mkdir, rm, writeFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterEach, expect, it } from "vitest"; +import type { CopilotSession } from "../../src/index.js"; +import { RuntimeConnection } from "../../src/index.js"; +import { createSdkTestContext, getLegacyCliPathForTests } from "./harness/sdkTestContext.js"; +import { retry } from "./harness/sdkTestHelper.js"; + +const cliPath = await getLegacyCliPathForTests(); +const cliDistDirectory = process.env.COPILOT_EXTENSION_SDK_PATH + ? dirname(process.env.COPILOT_EXTENSION_SDK_PATH) + : dirname(cliPath); +const sdkDistDirectory = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "dist"); +const extensionLaunchProvider = { + resolve: async (request: { modulePath: string }) => ({ + launch: { + executable: "node", + args: [join(cliDistDirectory, "preloads", "extension_bootstrap.mjs")], + env: { + COPILOT_CLI_DIST_DIR: cliDistDirectory, + EXTENSION_PATH: request.modulePath, + }, + }, + }), +}; +const { createClient, env, workDir } = await createSdkTestContext({ + copilotClientOptions: { + connection: RuntimeConnection.forStdio({ path: cliPath }), + env: { COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS" }, + extensionLaunchProvider, + }, +}); + +afterEach(async () => { + await rm(join(env.COPILOT_HOME, "extensions"), { recursive: true, force: true }); +}); + +async function installExtension( + directory: string, + name: string, + body = `import { joinSession } from "@github/copilot-sdk/extension"; +await joinSession({}); +setInterval(() => {}, 60_000);` +): Promise { + const extensionDir = join(directory, name); + await mkdir(extensionDir, { recursive: true }); + await writeFile(join(extensionDir, "extension.mjs"), body); + return name; +} + +async function withExtensionSession( + action: (session: CopilotSession) => Promise +): Promise { + const client = createClient({ extensionLaunchProvider }); + try { + const session = await client.createSession({ + enableConfigDiscovery: true, + extensionSdkPath: sdkDistDirectory, + disabledMcpServers: ["github-mcp-server"], + onPermissionRequest: () => ({ + kind: "approve-once", + approvedInteractively: true, + }), + }); + try { + await action(session); + } finally { + await session.disconnect(); + } + } finally { + expect(await client.stop()).toEqual([]); + } +} + +async function waitForExtension(session: CopilotSession, id: string, status: string) { + let found: + | Awaited>["extensions"][number] + | undefined; + await retry( + `observe ${id} in ${status} status (last: ${found?.status ?? "absent"})`, + async () => { + found = (await session.rpc.extensions.list()).extensions.find((ext) => ext.id === id); + expect(found?.status).toBe(status); + }, + 300, + 100 + ); + return found!; +} + +it( + "discovers and runs project and user extensions independently", + { timeout: 120_000 }, + async () => { + execFileSync("git", ["init", "--quiet"], { cwd: workDir }); + const projectName = await installExtension( + join(workDir, ".github", "extensions"), + `project-${randomUUID()}` + ); + const userName = await installExtension( + join(env.COPILOT_HOME, "extensions"), + `user-${randomUUID()}` + ); + await withExtensionSession(async (session) => { + const project = await waitForExtension(session, `project:${projectName}`, "running"); + const user = await waitForExtension(session, `user:${userName}`, "running"); + expect(project).toMatchObject({ name: projectName, source: "project" }); + expect(user).toMatchObject({ name: userName, source: "user" }); + expect(project.pid).toBeGreaterThan(0); + expect(user.pid).toBeGreaterThan(0); + expect(project.pid).not.toBe(user.pid); + }); + } +); + +it( + "disables, re-enables, and preserves disabled status across reload", + { timeout: 120_000 }, + async () => { + const name = await installExtension( + join(env.COPILOT_HOME, "extensions"), + `toggle-${randomUUID()}` + ); + const id = `user:${name}`; + await withExtensionSession(async (session) => { + await waitForExtension(session, id, "running"); + await session.rpc.extensions.disable({ id }); + expect((await waitForExtension(session, id, "disabled")).pid ?? null).toBeNull(); + await session.rpc.extensions.reload(); + expect((await waitForExtension(session, id, "disabled")).pid ?? null).toBeNull(); + await session.rpc.extensions.enable({ id }); + expect((await waitForExtension(session, id, "running")).pid).toBeGreaterThan(0); + }); + } +); + +it("reload discovers an extension added after session creation", { timeout: 120_000 }, async () => { + await withExtensionSession(async (session) => { + const name = await installExtension( + join(env.COPILOT_HOME, "extensions"), + `new-${randomUUID()}` + ); + await retry("reload after extension controller initialization", async () => { + await session.rpc.extensions.reload(); + }); + expect((await waitForExtension(session, `user:${name}`, "running")).source).toBe("user"); + }); +}); + +it("reports a failed extension without losing other extensions", { timeout: 120_000 }, async () => { + const base = join(env.COPILOT_HOME, "extensions"); + const bad = await installExtension( + base, + `failing-${randomUUID()}`, + "throw new Error('intentional startup failure');" + ); + const good = await installExtension(base, `running-${randomUUID()}`); + await withExtensionSession(async (session) => { + expect((await waitForExtension(session, `user:${bad}`, "failed")).source).toBe("user"); + expect((await waitForExtension(session, `user:${good}`, "running")).pid).toBeGreaterThan(0); + }); +}); + +it( + "persists server extension enablement only for future sessions", + { timeout: 120_000 }, + async () => { + const name = await installExtension( + join(env.COPILOT_HOME, "extensions"), + `persistent-${randomUUID()}` + ); + const id = `user:${name}`; + const client = createClient({ extensionLaunchProvider }); + const config = { + enableConfigDiscovery: true, + extensionSdkPath: sdkDistDirectory, + disabledMcpServers: ["github-mcp-server"], + onPermissionRequest: () => ({ + kind: "approve-once" as const, + approvedInteractively: true, + }), + }; + try { + const active = await client.createSession(config); + try { + await waitForExtension(active, id, "running"); + await client.rpc.user.settings.reload(); + expect((await client.rpc.extensions.discover()).extensions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id, enabled: true, source: "user" }), + ]) + ); + expect((await client.rpc.plugins.list()).plugins).toEqual([]); + + await client.rpc.extensions.disable({ ids: [id] }); + expect((await waitForExtension(active, id, "running")).pid).toBeGreaterThan(0); + + const disabledSession = await client.createSession(config); + try { + expect( + (await waitForExtension(disabledSession, id, "disabled")).pid ?? null + ).toBeNull(); + await client.rpc.extensions.enable({ ids: [id] }); + expect( + (await waitForExtension(disabledSession, id, "disabled")).pid ?? null + ).toBeNull(); + + const enabledSession = await client.createSession(config); + try { + expect( + (await waitForExtension(enabledSession, id, "running")).pid + ).toBeGreaterThan(0); + } finally { + await enabledSession.disconnect(); + } + } finally { + await disabledSession.disconnect(); + } + } finally { + await active.disconnect(); + } + } finally { + expect(await client.stop()).toEqual([]); + } + } +); diff --git a/nodejs/test/e2e/rpc_mcp_and_skills.e2e.test.ts b/nodejs/test/e2e/rpc_mcp_and_skills.e2e.test.ts index 9b0460c61d..82f5afb685 100644 --- a/nodejs/test/e2e/rpc_mcp_and_skills.e2e.test.ts +++ b/nodejs/test/e2e/rpc_mcp_and_skills.e2e.test.ts @@ -316,6 +316,106 @@ describe("Session MCP and skills RPC", async () => { await session.disconnect(); }); + it("should paginate and get MCP prompts without interpreting their content", async () => { + const serverName = "rpc-prompts-server"; + const fixtures = JSON.parse( + fs.readFileSync(path.join(TEST_HARNESS_DIR, "mcp-prompt-fixtures.json"), "utf8") + ); + const session = await client.createSession({ + onPermissionRequest: approveAll, + mcpServers: createTestMcpServers(serverName), + }); + const changedServers: string[] = []; + const unsubscribe = session.on("mcp.prompts.list_changed", (event) => { + changedServers.push(event.data.serverName); + }); + try { + await waitForMcpServerStatus(session, serverName); + const first = await session.rpc.mcp.prompts.list({ serverName }); + expect(first).toEqual(fixtures.firstPage); + const second = await session.rpc.mcp.prompts.list({ + serverName, + cursor: first.nextCursor, + }); + expect(second).toEqual(fixtures.secondPage); + expect(second.nextCursor).toBeUndefined(); + await expectFailure( + () => session.rpc.mcp.prompts.list({ serverName, cursor: "invalid-cursor" }), + "Unknown prompt cursor" + ); + + const rich = await session.rpc.mcp.prompts.get({ + serverName, + promptName: "rich", + arguments: { topic: "opaque content", style: "", extra: "日本語" }, + }); + expect(rich).toEqual(fixtures.richPrompt); + + const isolatedSession = await client.createSession({ onPermissionRequest: approveAll }); + try { + await expect( + isolatedSession.rpc.mcp.prompts.list({ serverName }) + ).rejects.toThrow(); + await expect( + isolatedSession.rpc.mcp.prompts.get({ + serverName, + promptName: "rich", + arguments: { topic: "opaque content" }, + }) + ).rejects.toThrow(); + expect(await session.rpc.mcp.prompts.list({ serverName })).toEqual( + fixtures.firstPage + ); + } finally { + await isolatedSession.disconnect(); + } + + const argumentSets: (Record | undefined)[] = [ + undefined, + {}, + { value: "日本語", empty: "" }, + ]; + for (const args of argumentSets) { + const result = await session.rpc.mcp.prompts.get({ + serverName, + promptName: "echo", + ...(args === undefined ? {} : { arguments: args }), + }); + expect(result).toEqual({ + messages: [ + { + role: "user", + content: { type: "text", text: expect.any(String) }, + }, + ], + }); + const content = result.messages[0].content as { text: string }; + expect(JSON.parse(content.text)).toEqual(args ?? null); + } + await expectFailure( + () => session.rpc.mcp.prompts.get({ serverName, promptName: "unknown" }), + "Unknown prompt" + ); + await expectFailure( + () => + session.rpc.mcp.prompts.get({ serverName, promptName: "rich", arguments: {} }), + "Missing required argument" + ); + + changedServers.length = 0; + await session.rpc.mcp.prompts.get({ serverName, promptName: "refresh" }); + await expect.poll(() => changedServers, { timeout: 30_000 }).toContain(serverName); + const refreshed = await session.rpc.mcp.prompts.list({ + serverName, + cursor: first.nextCursor, + }); + expect(refreshed.prompts).toEqual([...fixtures.secondPage.prompts, { name: "added" }]); + } finally { + unsubscribe(); + await session.disconnect(); + } + }); + it("should set mcp env value mode and remove github server", async () => { const serverName = "github"; const mcpServers = createTestMcpServers(serverName); diff --git a/nodejs/test/e2e/rpc_mcp_config.e2e.test.ts b/nodejs/test/e2e/rpc_mcp_config.e2e.test.ts index 95694a8c63..813a56bbd9 100644 --- a/nodejs/test/e2e/rpc_mcp_config.e2e.test.ts +++ b/nodejs/test/e2e/rpc_mcp_config.e2e.test.ts @@ -54,6 +54,14 @@ describe("Server-scoped MCP config RPC", () => { await client.rpc.mcp.config.add({ name: serverName, config }); const afterAdd = await client.rpc.mcp.config.list(); expect(afterAdd.servers[serverName]).toBeDefined(); + const discovered = await client.rpc.mcp.discover({ + workingDirectory: process.cwd(), + includeEffectiveSource: true, + }); + expect(discovered.servers.find((server) => server.name === serverName)).toMatchObject({ + enabled: true, + effectiveSource: expect.anything(), + }); await client.rpc.mcp.config.update({ name: serverName, config: updatedConfig }); const afterUpdate = await client.rpc.mcp.config.list(); @@ -65,7 +73,17 @@ describe("Server-scoped MCP config RPC", () => { expect(updated.args?.[0]).toBe("--version"); await client.rpc.mcp.config.disable({ names: [serverName] }); + expect( + (await client.rpc.mcp.discover({ workingDirectory: process.cwd() })).servers.find( + (server) => server.name === serverName + )?.enabled + ).toBe(false); await client.rpc.mcp.config.enable({ names: [serverName] }); + expect( + (await client.rpc.mcp.discover({ workingDirectory: process.cwd() })).servers.find( + (server) => server.name === serverName + )?.enabled + ).toBe(true); } finally { await client.rpc.mcp.config.remove({ name: serverName }); } diff --git a/nodejs/test/e2e/rpc_server.e2e.test.ts b/nodejs/test/e2e/rpc_server.e2e.test.ts index 7bab043a1a..d89b2c3e1e 100644 --- a/nodejs/test/e2e/rpc_server.e2e.test.ts +++ b/nodejs/test/e2e/rpc_server.e2e.test.ts @@ -108,6 +108,62 @@ describe("Server-scoped RPC", async () => { await expect(client.rpc.managedSettings.clearCache()).resolves.toBeNull(); }); + it.skipIf(isInProcessTransport)("should round trip sessionless managed settings", async () => { + const policyPath = path.join(workDir, "managed-settings.json"); + const policy = { model: "gpt-5.4", autoTier: "balance" }; + fs.writeFileSync(policyPath, JSON.stringify(policy)); + const policyClient = new CopilotClient({ + workingDirectory: workDir, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + useLoggedInUser: false, + env: { + ...env, + GH_TOKEN: "", + GITHUB_TOKEN: "", + COPILOT_GITHUB_TOKEN: "", + GITHUB_COPILOT_API_TOKEN: "", + COPILOT_HMAC_KEY: "", + CAPI_HMAC_KEY: "", + COPILOT_E2E_TEST_HOOKS: "1", + COPILOT_TEST_MANAGED_SETTINGS_FILE_PATH: policyPath, + }, + }); + onTestFinished(() => policyClient.stop()); + await policyClient.start(); + + const api = policyClient.rpc.managedSettings; + const schema = await api.schema(); + expect(schema.runtimeVersion).toEqual(expect.any(String)); + expect(schema.schema).toHaveProperty("properties.model"); + + const validated = await api.validate({ content: policy }); + expect(validated.valid).toBe(true); + expect(validated.settings).toEqual(policy); + expect(validated.diagnostics).toEqual([]); + + const resolved = await api.resolve({}); + expect(resolved.account).toBeUndefined(); + expect(resolved.resolved.deviceManaged).toBe(true); + expect(resolved.values?.model).toBe(policy.model); + const device = resolved.layers.find((layer) => layer.source === "device"); + expect(device?.settings).toMatchObject(policy); + + const composed = await api.compose({ + layers: [{ source: "device", settings: device?.settings }], + }); + expect(composed.resolved.settings).toEqual(resolved.resolved.settings); + expect(composed.values).toEqual(resolved.values); + expect(composed.diagnostics).toEqual([]); + await expect( + api.compose({ + // @ts-expect-error Exercise invalid wire input through the real server. + layers: [{ source: "device", settings: null }], + }) + ).rejects.toMatchObject({ code: -32602 }); + const absent = await api.compose({ layers: [{ source: "device" }] }); + expect(absent.resolved.source).toBe("none"); + }); + it("should reject llm inference response frames for missing request", async () => { await client.start(); @@ -222,7 +278,15 @@ describe("Server-scoped RPC", async () => { const host = await client.rpc.sandbox.getHostSupport(); expect(host.reason === undefined).toBe(host.supported); expect(host.capabilities.map((capability) => capability.name).sort()).toEqual( - host.supported ? ["denied_paths", "network", "network_filtering", "shell"] : [] + host.supported + ? [ + "denied_paths", + "filesystem_enumeration", + "network", + "network_filtering", + "shell", + ] + : [] ); for (const capability of host.capabilities) { expect(capability.reason === undefined).toBe(capability.supported); @@ -356,6 +420,42 @@ describe("Server-scoped RPC", async () => { // The server-side close disposes the session; do not call session.disconnect(). }); + it.skipIf(isInProcessTransport)( + "should observe a session held by another runtime and release its lock", + async () => { + const sessionId = randomUUID(); + const otherClient = createClientWithEnv({}); + const otherSession = await otherClient.createSession({ + sessionId, + workingDirectory: createUniqueWorkDirectory("server-rpc-in-use"), + onPermissionRequest: () => ({ kind: "approve-once" }), + }); + + try { + await client.start(); + await waitForCondition( + async () => + ( + await client.rpc.sessions.checkInUse({ sessionIds: [sessionId] }) + ).inUse.includes(sessionId), + { timeoutMessage: `Session ${sessionId} was not reported in use` } + ); + + await otherClient.rpc.sessions.releaseLock({ sessionId }); + await waitForCondition( + async () => + !( + await client.rpc.sessions.checkInUse({ sessionIds: [sessionId] }) + ).inUse.includes(sessionId), + { timeoutMessage: `Session ${sessionId} was still reported in use` } + ); + } finally { + await otherSession.disconnect(); + await otherClient.stop(); + } + } + ); + it("should prune dry-run and bulkDelete persisted session", async () => { const sessionId = randomUUID(); const missingSessionId = randomUUID(); diff --git a/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts b/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts index 7b88af7e2d..a7e62c185f 100644 --- a/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts +++ b/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts @@ -6,6 +6,9 @@ import { describe, expect, it } from "vitest"; import type { CopilotSession } from "../../src/index.js"; import { approveAll, CopilotClient, RuntimeConnection } from "../../src/index.js"; import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js"; +import { isByokBackend } from "./harness/testBackend"; + +const capiIt = it.skipIf(isByokBackend); describe("Session-scoped state extras RPC", async () => { const { copilotClient: client, env, openAiEndpoint, workDir } = await createSdkTestContext(); @@ -103,7 +106,8 @@ describe("Session-scoped state extras RPC", async () => { } }); - it("should add byok provider and model at runtime", { timeout: 120_000 }, async () => { + // Owns a multi-provider registry rather than the matrix's single provider. + capiIt("should add byok provider and model at runtime", { timeout: 120_000 }, async () => { const session = await createSession(); try { const providerName = `sdk-runtime-provider-${Date.now()}-${Math.random().toString(36).slice(2)}`; diff --git a/nodejs/test/e2e/rpc_shell_and_fleet.e2e.test.ts b/nodejs/test/e2e/rpc_shell_and_fleet.e2e.test.ts index 9344277b05..521a4b0c1b 100644 --- a/nodejs/test/e2e/rpc_shell_and_fleet.e2e.test.ts +++ b/nodejs/test/e2e/rpc_shell_and_fleet.e2e.test.ts @@ -5,11 +5,13 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; +import { randomUUID } from "node:crypto"; import { describe, expect, it } from "vitest"; import { z } from "zod"; import { approveAll, defineTool } from "../../src/index.js"; import type { CopilotSession, SessionEvent } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; describe("Shell and fleet RPC", async () => { const { copilotClient: client, workDir } = await createSdkTestContext(); @@ -96,6 +98,139 @@ describe("Shell and fleet RPC", async () => { await session.disconnect(); }); + it("should honor custom cwd for shell exec", async () => { + const session = await client.createSession({ onPermissionRequest: approveAll }); + const cwd = path.join(workDir, `shell-cwd-${randomUUID()}`); + fs.mkdirSync(cwd); + try { + const result = await session.rpc.shell.exec({ + command: createWriteFileCommand("marker.txt", "custom-cwd"), + cwd, + }); + expect(result.processId).toBeTruthy(); + await waitForFileText(path.join(cwd, "marker.txt"), "custom-cwd"); + } finally { + await session.disconnect(); + } + }); + + it("should return false when killing an unknown shell process", async () => { + const session = await client.createSession({ onPermissionRequest: approveAll }); + try { + expect( + (await session.rpc.shell.kill({ processId: `unknown-${randomUUID()}` })).killed + ).toBe(false); + } finally { + await session.disconnect(); + } + }); + + it.each(["SIGTERM", "SIGKILL"] as const)( + "should release shell process after %s", + async (signal) => { + const session = await client.createSession({ onPermissionRequest: approveAll }); + const command = + os.platform() === "win32" + ? 'powershell -NoLogo -NoProfile -Command "Start-Sleep -Seconds 60"' + : "sleep 60"; + try { + const result = await session.rpc.shell.exec({ command, cwd: workDir }); + expect(result.processId).toBeTruthy(); + expect( + (await session.rpc.shell.kill({ processId: result.processId, signal })).killed + ).toBe(true); + await waitForCondition( + async () => + !(await session.rpc.shell.kill({ processId: result.processId })).killed, + { timeoutMessage: `Process ${result.processId} remains after ${signal}` } + ); + } finally { + await session.disconnect(); + } + } + ); + + it( + "should stop a timed-out shell command before its final marker", + { timeout: 60_000 }, + async () => { + const session = await client.createSession({ onPermissionRequest: approveAll }); + const started = path.join(workDir, `shell-started-${randomUUID()}.txt`); + const completed = path.join(workDir, `shell-completed-${randomUUID()}.txt`); + const timeout = os.platform() === "win32" ? 10_000 : 1_000; + const command = + os.platform() === "win32" + ? `echo started>"${started}" & ping.exe -n 31 127.0.0.1 >nul & echo completed>"${completed}"` + : `printf started > '${started}'; sleep 30; printf completed > '${completed}'`; + try { + const result = await session.rpc.shell.exec({ command, cwd: workDir, timeout }); + expect(result.processId).toBeTruthy(); + await waitForCondition(() => fs.existsSync(started), { + timeoutMessage: `Timed-out shell command did not start: ${started}`, + }); + // The start marker acknowledges the phase before the deadline is observed. + await new Promise((resolve) => setTimeout(resolve, timeout + 2_000)); + expect((await session.rpc.shell.kill({ processId: result.processId })).killed).toBe( + false + ); + expect(fs.existsSync(completed)).toBe(false); + } finally { + await session.disconnect(); + } + } + ); + + it("should accept a missing shell command and clean up after it exits", async () => { + const session = await client.createSession({ onPermissionRequest: approveAll }); + const marker = path.join(workDir, `shell-missing-${randomUUID()}.txt`); + const missingCommand = `not-a-command-${randomUUID()}`; + const command = + os.platform() === "win32" + ? `${missingCommand} & echo done>"${marker}" & exit /b 1` + : `${missingCommand}; code=$?; printf done > '${marker}'; exit $code`; + try { + const result = await session.rpc.shell.exec({ command, cwd: workDir }); + expect(result.processId).toBeTruthy(); + await waitForFileText(marker, "done"); + // The shell may still be flushing stderr when the final marker is written. + await new Promise((resolve) => setTimeout(resolve, 1_000)); + expect((await session.rpc.shell.kill({ processId: result.processId })).killed).toBe( + false + ); + } finally { + await session.disconnect(); + } + }); + + it.each(["stderr", "large-stdout"] as const)( + "should clean up shell process after %s output", + async (outputKind) => { + const session = await client.createSession({ onPermissionRequest: approveAll }); + const marker = path.join(workDir, `shell-${outputKind}-${randomUUID()}.txt`); + const command = + outputKind === "stderr" + ? os.platform() === "win32" + ? `powershell -NoLogo -NoProfile -Command "[Console]::Error.WriteLine('boom'); Set-Content -LiteralPath '${marker}' -Value done; exit 2"` + : `echo boom 1>&2; printf done > '${marker}'; exit 2` + : os.platform() === "win32" + ? `powershell -NoLogo -NoProfile -Command "Write-Host ('x' * 71680); Set-Content -LiteralPath '${marker}' -Value done"` + : `printf '%71680s' '' | tr ' ' '='; printf done > '${marker}'`; + try { + const result = await session.rpc.shell.exec({ command, cwd: workDir }); + expect(result.processId).toBeTruthy(); + await waitForFileText(marker, "done"); + // The process map is updated after the output stream closes. + await new Promise((resolve) => setTimeout(resolve, 2_000)); + expect((await session.rpc.shell.kill({ processId: result.processId })).killed).toBe( + false + ); + } finally { + await session.disconnect(); + } + }, + 60_000 + ); + it("should start fleet and complete custom tool task", { timeout: 180_000 }, async () => { const markerPath = path.join( workDir, diff --git a/nodejs/test/e2e/rpc_shell_user_requested.e2e.test.ts b/nodejs/test/e2e/rpc_shell_user_requested.e2e.test.ts index 961771f78b..b8e3394313 100644 --- a/nodejs/test/e2e/rpc_shell_user_requested.e2e.test.ts +++ b/nodejs/test/e2e/rpc_shell_user_requested.e2e.test.ts @@ -3,15 +3,15 @@ *--------------------------------------------------------------------------------------------*/ import { randomUUID } from "node:crypto"; -import { existsSync, rmSync } from "node:fs"; +import { existsSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { approveAll } from "../../src/index.js"; +import { approveAll, type SessionEvent } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; import { waitForCondition } from "./harness/sdkTestHelper.js"; describe("User-requested shell RPC", async () => { - const { copilotClient: client, homeDir } = await createSdkTestContext(); + const { copilotClient: client, homeDir, workDir } = await createSdkTestContext(); function compactUuid(): string { return randomUUID().replace(/-/g, ""); @@ -88,6 +88,214 @@ describe("User-requested shell RPC", async () => { } }); + it.for([0, 7])( + "dispatches queued exit after user shell completes with code %i", + { timeout: 120_000 }, + async (exitCode, { onTestFinished }) => { + const session = await client.createSession({ onPermissionRequest: approveAll }); + const markerPath = join(homeDir, `shell-started-${compactUuid()}`); + const releasePath = join(homeDir, `shell-release-${compactUuid()}`); + let executeTask: ReturnType | undefined; + let interestHandle: string | undefined; + let unsubscribe: (() => void) | undefined; + onTestFinished(async () => { + writeFileSync(releasePath, "release"); + try { + if (executeTask) { + await withTimeout(executeTask, 30_000, "Shell completion did not drain."); + } + } finally { + unsubscribe?.(); + try { + if (interestHandle !== undefined) { + await session.rpc.eventLog.releaseInterest({ handle: interestHandle }); + } + } finally { + await session.disconnect(); + } + } + }); + + const interest = await session.rpc.eventLog.registerInterest({ + eventType: "command.queued", + }); + interestHandle = interest.handle; + const queued = + Promise.withResolvers>(); + const idle = Promise.withResolvers(); + let commandDispatched = false; + let dispatchedBeforeRelease = false; + unsubscribe = session.on((event) => { + if (event.type === "command.queued" && event.data.command === "/exit") { + commandDispatched = true; + dispatchedBeforeRelease ||= !existsSync(releasePath); + queued.resolve(event); + } else if (event.type === "session.idle" && commandDispatched) { + idle.resolve(); + } + }); + const handled = queued.promise.then((event) => + session.rpc.commands.respondToQueuedCommand({ + requestId: event.data.requestId, + result: { handled: true }, + }) + ); + handled.catch(() => {}); + + const command = + process.platform === "win32" + ? `Set-Content -LiteralPath ${quotePowerShell(markerPath)} -Value 'running'; while (-not (Test-Path -LiteralPath ${quotePowerShell(releasePath)})) { Start-Sleep -Milliseconds 50 }; exit ${exitCode}` + : `echo running > ${quoteSh(markerPath)}; while [ ! -f ${quoteSh(releasePath)} ]; do sleep 0.05; done; exit ${exitCode}`; + executeTask = session.rpc.shell.executeUserRequested({ + requestId: `req-${compactUuid()}`, + command, + }); + executeTask.catch(() => {}); + await waitForFileExists(markerPath); + expect((await session.rpc.commands.enqueue({ command: "/exit" })).queued).toBe(true); + + // Enqueue has acknowledged the command while the real subprocess is + // held; inspect the queue rather than using a delay to prove parking. + expect((await session.rpc.queue.pendingItems()).items).toEqual([ + expect.objectContaining({ kind: "command", displayText: "/exit" }), + ]); + + writeFileSync(releasePath, "release"); + const result = await withTimeout( + executeTask, + 30_000, + "Shell completion did not resume the queued exit." + ); + expect(result.exitCode).toBe(exitCode); + expect(result.success).toBe(exitCode === 0); + expect(await withTimeout(handled, 30_000, "Queued exit was not dispatched.")).toEqual({ + success: true, + }); + expect(dispatchedBeforeRelease).toBe(false); + await withTimeout(idle.promise, 30_000, "Session did not settle after queued exit."); + expect((await session.rpc.queue.pendingItems()).items).toEqual([]); + } + ); + + it( + "streams separate stdout and stderr before a controlled exit and never replays chunks", + { + timeout: 120_000, + }, + async () => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + workingDirectory: workDir, + }); + const requestId = `stream-${compactUuid()}`; + const releasePath = join(workDir, "release"); + const events: SessionEvent[] = []; + let stdout = ""; + let stderr = ""; + let released = false; + let settled = false; + const unsubscribe = session.on((event) => { + events.push(event); + if (event.type !== "tool.shell_output") { + return; + } + if ((event.data.stream ?? "stdout") === "stdout") { + stdout += event.data.text; + } else if (event.data.stream === "stderr") { + stderr += event.data.text; + } + if ( + !released && + stdout.includes("shell-stream-live-out") && + stderr.includes("shell-stream-live-err") + ) { + released = true; + writeFileSync(releasePath, ""); + } + }); + // The public shell RPC exercises live delivery without a model choosing + // a tool or requiring a synthesized model response. + const execution = session.rpc.shell + .executeUserRequested({ + requestId, + command: `node -e "const fs=require('fs'),newline=String.fromCharCode(10);fs.watch('.',()=>{if(fs.existsSync('release')){fs.writeSync(1,'shell-stream-tail'+newline);process.exit(0)}});fs.writeSync(1,'shell-stream-live-out'+newline);fs.writeSync(2,'shell-stream-live-err'+newline)"`, + }) + .finally(() => { + settled = true; + }); + try { + const result = await withTimeout( + execution, + 75_000, + "Timed out waiting for live stdout/stderr to release the shell command." + ); + expect(result.success).toBe(true); + expect(result.exitCode).toBe(0); + expect(released).toBe(true); + expect(stdout).toBe("shell-stream-live-out\nshell-stream-tail\n"); + expect(stderr).toBe("shell-stream-live-err\n"); + const chunks = events.filter((event) => event.type === "tool.shell_output"); + expect(chunks.map((event) => event.data.sequence)).toEqual( + chunks.map((_, index) => index) + ); + expect(chunks.every((event) => event.ephemeral === true)).toBe(true); + expect(chunks.every((event) => event.data.toolCallId === result.toolCallId)).toBe( + true + ); + expect(result.toolCallId).toBeTruthy(); + const completionIndex = events.findIndex( + (event) => + event.type === "tool.execution_complete" && + event.data.toolCallId === result.toolCallId + ); + expect(completionIndex).toBeGreaterThan(0); + expect( + events + .slice(completionIndex) + .some( + (event) => + event.type === "tool.shell_output" && + event.data.toolCallId === result.toolCallId + ) + ).toBe(false); + expect( + events.some( + (event) => + event.type === "tool.execution_partial_result" && + event.data.toolCallId === result.toolCallId + ) + ).toBe(true); + let history: SessionEvent[] = []; + await waitForCondition( + async () => { + history = await session.getEvents(); + return history.some( + (event) => + event.type === "tool.execution_complete" && + event.data.toolCallId === result.toolCallId + ); + }, + { timeoutMessage: "Shell completion did not appear in session history." } + ); + expect(history.some((event) => event.type === "tool.shell_output")).toBe(false); + } finally { + unsubscribe(); + try { + if (!settled) { + await session.rpc.shell.cancelUserRequested({ requestId }); + await withTimeout( + execution, + 30_000, + "Timed out draining the streaming shell command." + ); + } + } finally { + await session.disconnect(); + } + } + } + ); + it("should cancel user requested shell command", { timeout: 120_000 }, async () => { const session = await client.createSession({ onPermissionRequest: approveAll }); const markerPath = join(homeDir, `shell-cancel-${compactUuid()}.txt`); diff --git a/nodejs/test/e2e/rpc_surface_coverage.e2e.test.ts b/nodejs/test/e2e/rpc_surface_coverage.e2e.test.ts index e03020e4a8..4995e08094 100644 --- a/nodejs/test/e2e/rpc_surface_coverage.e2e.test.ts +++ b/nodejs/test/e2e/rpc_surface_coverage.e2e.test.ts @@ -102,6 +102,20 @@ function handle(message) { return; } + if (message.method === "session.instructions.reload") { + writeResponse(message.id, { sources: [] }); + return; + } + + if (message.method === "session.customizations.reload") { + writeResponse(message.id, { + warnings: [], + errors: [], + outcomes: [{ subsystem: "instructions", status: "reloaded" }] + }); + return; + } + writeResponse(message.id, { method: message.method, params: message.params ?? null, @@ -313,7 +327,21 @@ describe("Generated RPC surface coverage", () => { ...collectRuntimeFunctions(session.rpc, "session"), ]); - expect(inventory).toHaveLength(372); + expect(inventory).toHaveLength(395); + const sessionProviderMethods = [ + "providers.getCatalog", + "providers.discover", + "providers.getStatus", + "providers.models.list", + "providers.models.prepareConfiguration", + ]; + for (const path of sessionProviderMethods) { + const wireMethod = `session.${path}`; + expect( + inventory.filter((method) => method.wireMethod === wireMethod), + `Missing generated session RPC ${wireMethod}` + ).toEqual([expect.objectContaining({ scope: "session", path })]); + } const boundInstallationMethods = [ "mcp.prepareInstall", "mcp.applyInstall", @@ -359,6 +387,19 @@ describe("Generated RPC surface coverage", () => { continue; } + if (method.wireMethod === "session.instructions.reload") { + expect(result).toEqual({ sources: [] }); + continue; + } + if (method.wireMethod === "session.customizations.reload") { + expect(result).toEqual({ + warnings: [], + errors: [], + outcomes: [{ subsystem: "instructions", status: "reloaded" }], + }); + continue; + } + expect(result).toMatchObject({ method: method.wireMethod, state: { @@ -402,6 +443,11 @@ describe("Generated RPC surface coverage", () => { await session.rpc.mode.set({ mode: "interactive" }); expect(await session.rpc.mode.get()).toBe("interactive"); + expect((await session.rpc.instructions.reload()).sources).toEqual([]); + expect((await session.rpc.customizations.reload()).outcomes).toEqual([ + { subsystem: "instructions", status: "reloaded" }, + ]); + await expect( client.rpc.ping({ message: "error", __forceError: true } as never) ).rejects.toMatchObject({ @@ -424,5 +470,11 @@ describe("Generated RPC surface coverage", () => { true ); } + for (const wireMethod of ["session.instructions.reload", "session.customizations.reload"]) { + expect(captured.filter((request) => request.method === wireMethod)).toEqual([ + { method: wireMethod, params: { sessionId: "rpc-surface-session" } }, + { method: wireMethod, params: { sessionId: "rpc-surface-session" } }, + ]); + } }); }); diff --git a/nodejs/test/e2e/rpc_tasks_and_handlers.e2e.test.ts b/nodejs/test/e2e/rpc_tasks_and_handlers.e2e.test.ts index cb41c69e68..c2e62d3bcd 100644 --- a/nodejs/test/e2e/rpc_tasks_and_handlers.e2e.test.ts +++ b/nodejs/test/e2e/rpc_tasks_and_handlers.e2e.test.ts @@ -3,11 +3,53 @@ *--------------------------------------------------------------------------------------------*/ import { describe, expect, it } from "vitest"; +import { z } from "zod"; import { approveAll } from "../../src/index.js"; +import type { SessionEvent, CopilotSession } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; +import { isByokBackend } from "./harness/testBackend"; + +const capiIt = it.skipIf(isByokBackend); describe("Session tasks RPC and pending handlers", async () => { - const { copilotClient: client } = await createSdkTestContext(); + const { + copilotClient: client, + openAiEndpoint, + createClient, + } = await createSdkTestContext({ + modelNames: { "claude-sonnet-5": "Claude Sonnet 5" }, + }); + + async function modelRequestsForPrompt(promptEnding: string) { + return (await openAiEndpoint.getRequests()) + .filter(({ method, url }) => method === "POST" && url.includes("/chat/completions")) + .map(({ body }) => + z + .object({ + messages: z.array(z.object({ role: z.string(), content: z.unknown() })), + model: z.string(), + reasoning_effort: z.string().optional(), + }) + .parse(JSON.parse(body)) + ) + .filter((request) => + request.messages.some( + (message) => + message.role === "user" && + typeof message.content === "string" && + message.content.trim().endsWith(promptEnding) + ) + ); + } + + async function getAgentTask(session: CopilotSession, agentId: string) { + const task = (await session.rpc.tasks.list()).tasks.find((entry) => entry.id === agentId); + if (task && task.type !== "agent") { + throw new Error(`Expected agent task ${agentId}, received ${JSON.stringify(task)}`); + } + return task; + } async function assertImplementedFailure( action: () => Promise, @@ -74,7 +116,8 @@ describe("Session tasks RPC and pending handlers", async () => { await session.disconnect(); }); - it("should report implemented error for invalid task agent model", async () => { + // BYOK model IDs are user-defined; only CAPI validates them against its catalog. + capiIt("should report implemented error for invalid task agent model", async () => { const session = await client.createSession({ onPermissionRequest: approveAll }); await assertImplementedFailure( @@ -93,6 +136,356 @@ describe("Session tasks RPC and pending handlers", async () => { await session.disconnect(); }); + it( + "should start a general-purpose agent on the parent's model", + { timeout: 240_000 }, + async () => { + const parentModel = "claude-sonnet-5"; + const session = await client.createSession({ + onPermissionRequest: approveAll, + model: parentModel, + }); + const started: Extract[] = []; + const parentReplies: string[] = []; + const unsubscribe = session.on((event) => { + if (event.type === "subagent.started") { + started.push(event); + } else if (event.type === "assistant.message" && !event.agentId) { + parentReplies.push(event.data.content ?? ""); + } + }); + try { + // The token gives the parent's turn for the completion notification a reply to wait for. + expect( + ( + await session.sendAndWait({ + prompt: + "Reply with TASK_MODEL_READY exactly. When a background agent notification " + + "arrives later, reply with TASK_MODEL_NOTIFIED exactly.", + }) + )?.data.content + ).toContain("TASK_MODEL_READY"); + + const prompt = "Reply with TASK_MODEL_CHILD_DONE exactly."; + const { agentId } = await session.rpc.tasks.startAgent({ + agentType: "general-purpose", + prompt, + name: "sdk-inherited-model-agent", + description: "SDK inherited model coverage", + }); + const findTask = () => getAgentTask(session, agentId); + let lastTask: Awaited>; + await waitForCondition( + async () => { + lastTask = await findTask(); + return ( + !!lastTask && + ["completed", "idle"].includes(lastTask.status) && + (lastTask.latestResponse ?? lastTask.result ?? "").includes( + "TASK_MODEL_CHILD_DONE" + ) + ); + }, + { timeoutMs: 60_000 } + ).catch((error: unknown) => { + throw new Error(`Agent ${agentId} never settled: ${JSON.stringify(lastTask)}`, { + cause: error, + }); + }); + // The parent's reply to the notification ends the last turn this test records. + await waitForCondition( + () => parentReplies.some((reply) => reply.includes("TASK_MODEL_NOTIFIED")), + { timeoutMs: 60_000 } + ).catch((error: unknown) => { + throw new Error( + `Parent never answered the completion notification: ${JSON.stringify(parentReplies)}`, + { cause: error } + ); + }); + + const task = await findTask(); + // Provider-backed subagents carry an explicit model override; CAPI leaves it unset. + expect(task?.model ?? null).toBe(isByokBackend ? parentModel : null); + expect(task?.resolvedModel).toBe(parentModel); + expect(started.map((event) => event.data.model)).toEqual([parentModel]); + // Raw requests, scoped by this test's prompts: an earlier test's late + // request can land here, and its error response is not parsable. + const requestModelsMentioning = async (text: string) => + (await openAiEndpoint.getRequests()) + .filter((request) => request.body.includes(text)) + .map((request) => (JSON.parse(request.body) as { model?: string }).model); + const childModels = await requestModelsMentioning(prompt); + const parentModels = await requestModelsMentioning("TASK_MODEL_READY"); + expect(childModels.length).toBeGreaterThan(0); + expect(parentModels.length).toBeGreaterThan(0); + expect(new Set([...childModels, ...parentModels])).toEqual(new Set([parentModel])); + } finally { + unsubscribe(); + await session.disconnect(); + } + } + ); + + it("should start background agent and report task details", { timeout: 240_000 }, async () => { + const session = await client.createSession({ onPermissionRequest: approveAll }); + const replies: string[] = []; + const unsubscribe = session.on((event) => { + if (event.type === "assistant.message") { + replies.push(event.data.content ?? ""); + } + }); + try { + expect( + (await session.sendAndWait({ prompt: "Reply with TASK_AGENT_READY exactly." })) + ?.data.content + ).toContain("TASK_AGENT_READY"); + const prompt = "Reply with TASK_AGENT_DONE exactly."; + const started = await session.rpc.tasks.startAgent({ + agentType: "general-purpose", + prompt, + name: "sdk-background-agent", + description: "SDK background agent coverage", + }); + expect(started.agentId).toBeTruthy(); + + await waitForCondition( + async () => + (await session.rpc.tasks.list()).tasks.some( + (task) => task.id === started.agentId + ), + { timeoutMessage: `Background agent ${started.agentId} never appeared` } + ); + const task = await getAgentTask(session, started.agentId); + expect(task).toMatchObject({ + id: started.agentId, + agentType: "general-purpose", + prompt, + description: "SDK background agent coverage", + executionMode: "background", + canPromoteToBackground: false, + }); + expect(Number.isNaN(Date.parse(task!.startedAt))).toBe(false); + expect( + (await session.rpc.tasks.promoteToBackground({ id: started.agentId })).promoted + ).toBe(false); + + await waitForCondition( + () => replies.some((message) => message.includes("TASK_AGENT_DONE")), + { + timeoutMs: 60_000, + timeoutMessage: `Agent ${started.agentId} did not complete: ${JSON.stringify(replies)}`, + } + ); + await waitForCondition( + async () => { + const task = await getAgentTask(session, started.agentId); + return ( + !!task && + ["completed", "idle"].includes(task.status) && + (task.latestResponse ?? task.result ?? "").includes("TASK_AGENT_DONE") + ); + }, + { timeoutMs: 60_000, timeoutMessage: `Agent ${started.agentId} never settled` } + ); + const current = await getAgentTask(session, started.agentId); + expect(current?.latestResponse ?? current?.result).toContain("TASK_AGENT_DONE"); + if (current?.status === "idle") { + expect((await session.rpc.tasks.cancel({ id: started.agentId })).cancelled).toBe( + true + ); + } + const removed = await session.rpc.tasks.remove({ id: started.agentId }); + expect(removed.removed || current === undefined).toBe(true); + await waitForCondition( + async () => + !(await session.rpc.tasks.list()).tasks.some( + (entry) => entry.id === started.agentId + ), + { timeoutMessage: `Completed agent ${started.agentId} remains listed` } + ); + } finally { + unsubscribe(); + await session.disconnect(); + } + }); + + capiIt( + "should apply a custom agent's composed model and effort when started over RPC", + { timeout: 240_000 }, + async () => { + const recordingClient = createClient(); + try { + const session = await recordingClient.createSession({ + onPermissionRequest: approveAll, + customAgents: [ + { + name: "sdk-composed-model-agent", + description: "SDK composed model coverage", + prompt: "Reply with SDK_COMPOSED_AGENT_DONE exactly.", + model: "claude-sonnet-5:defaultReasoningEffort=high", + }, + ], + }); + const configurations: Array<{ + agentId?: string; + model: string; + reasoningEffort?: string; + }> = []; + const unsubscribe = session.on((event) => { + if (event.type === "subagent.configured") { + configurations.push({ agentId: event.agentId, ...event.data }); + } + }); + try { + const started = await session.rpc.tasks.startAgent({ + agentType: "sdk-composed-model-agent", + prompt: "Reply with SDK_COMPOSED_AGENT_DONE exactly.", + name: "sdk-composed-model-task", + }); + await waitForCondition( + () => configurations.some((event) => event.agentId === started.agentId), + { + timeoutMs: 60_000, + timeoutMessage: `Agent ${started.agentId} was never configured: ${JSON.stringify(configurations)}`, + } + ); + expect( + configurations.find((event) => event.agentId === started.agentId) + ).toMatchObject({ + model: "claude-sonnet-5", + reasoningEffort: "high", + }); + + await waitForCondition( + async () => { + const task = await getAgentTask(session, started.agentId); + return ( + !!task && + ["completed", "idle"].includes(task.status) && + (task.latestResponse ?? task.result ?? "").includes( + "SDK_COMPOSED_AGENT_DONE" + ) + ); + }, + { + timeoutMs: 60_000, + timeoutMessage: `Agent ${started.agentId} never completed`, + } + ); + const requests = await modelRequestsForPrompt( + "Reply with SDK_COMPOSED_AGENT_DONE exactly." + ); + expect(requests.length).toBeGreaterThan(0); + for (const request of requests) { + expect(request).toMatchObject({ + model: "claude-sonnet-5", + reasoning_effort: "high", + }); + } + } finally { + unsubscribe(); + } + } finally { + await recordingClient.stop(); + } + } + ); + + capiIt( + "should preserve a composed display-name effort in a model-issued task", + async () => { + const taskArguments: unknown[] = []; + const recordingClient = createClient(); + try { + const session = await recordingClient.createSession({ + model: "claude-sonnet-5", + reasoningEffort: "medium", + onPermissionRequest: approveAll, + hooks: { + onPreToolUse: async (input) => { + if (input.toolName === "task") { + taskArguments.push(input.toolArgs); + } + return { permissionDecision: "allow" }; + }, + }, + customAgents: [ + { + name: "sdk-display-model-agent", + description: "SDK display-name model coverage", + prompt: "Reply with SDK_DISPLAY_AGENT_DONE exactly.", + model: "Claude Sonnet 5:defaultReasoningEffort=high", + }, + ], + }); + const modelList = await session.rpc.model.list(); + expect(modelList.list).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: "claude-sonnet-5", name: "Claude Sonnet 5" }), + ]) + ); + const configurations: Array<{ + agentId?: string; + model: string; + reasoningEffort?: string; + }> = []; + const unsubscribe = session.on((event) => { + if (event.type === "subagent.configured") { + configurations.push({ agentId: event.agentId, ...event.data }); + } + }); + try { + const reply = await session.sendAndWait( + { + prompt: + 'Use the task tool with agent_type "sdk-display-model-agent", mode "sync", and prompt "Reply with SDK_DISPLAY_AGENT_DONE exactly.". ' + + "Do not specify model or reasoning_effort in the task arguments. " + + "After it completes, reply with SDK_DISPLAY_PARENT_DONE exactly.", + }, + 120_000 + ); + expect(reply?.data.content).toContain("SDK_DISPLAY_PARENT_DONE"); + expect(taskArguments).toHaveLength(1); + expect(taskArguments[0]).toMatchObject({ + agent_type: "sdk-display-model-agent", + mode: "sync", + }); + expect(taskArguments[0]).not.toHaveProperty("model"); + expect(taskArguments[0]).not.toHaveProperty("reasoning_effort"); + expect(configurations).toHaveLength(1); + const configuration = configurations[0]; + expect(configuration).toMatchObject({ + model: "claude-sonnet-5", + reasoningEffort: "high", + }); + expect(configuration.agentId).toBeTruthy(); + const requests = await modelRequestsForPrompt( + "Reply with SDK_DISPLAY_AGENT_DONE exactly." + ); + expect(requests.length).toBeGreaterThan(0); + for (const request of requests) { + expect(request).toMatchObject({ + model: "claude-sonnet-5", + reasoning_effort: "high", + }); + } + const parentRequests = await modelRequestsForPrompt( + "After it completes, reply with SDK_DISPLAY_PARENT_DONE exactly." + ); + expect(parentRequests.length).toBeGreaterThan(0); + for (const request of parentRequests) { + expect(request.reasoning_effort).toBe("medium"); + } + } finally { + unsubscribe(); + } + } finally { + await recordingClient.stop(); + } + }, + 240_000 + ); + it("should return expected results for missing pending handler requestIds", async () => { const session = await client.createSession({ onPermissionRequest: approveAll }); diff --git a/nodejs/test/e2e/rpc_workspace_checkpoints.e2e.test.ts b/nodejs/test/e2e/rpc_workspace_checkpoints.e2e.test.ts index f1d8050925..83ba34434c 100644 --- a/nodejs/test/e2e/rpc_workspace_checkpoints.e2e.test.ts +++ b/nodejs/test/e2e/rpc_workspace_checkpoints.e2e.test.ts @@ -2,13 +2,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { existsSync, readFileSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { existsSync, mkdtempSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; +import { randomUUID } from "node:crypto"; +import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; describe("Session workspace checkpoint RPC", async () => { - const { copilotClient: client } = await createSdkTestContext(); + const { copilotClient: client, createClient, workDir } = await createSdkTestContext(); it("should list no checkpoints for fresh session", async () => { const session = await client.createSession({ onPermissionRequest: approveAll }); @@ -35,21 +38,77 @@ describe("Session workspace checkpoint RPC", async () => { it.each(["session", "unstaged", "branch"] as const)( "should return typed workspace diff result for %s mode", async (mode) => { - const session = await client.createSession({ onPermissionRequest: approveAll }); + // Git-root discovery caches misses, so use a fresh path for each repo fixture. + const gitWorkDir = mkdtempSync(join(workDir, "diff-")); + const modifiedName = `modified-${randomUUID()}.txt`; + const deletedName = `deleted-${randomUUID()}.txt`; + execFileSync("git", ["init", "--quiet"], { cwd: gitWorkDir }); + writeFileSync(join(gitWorkDir, modifiedName), "before"); + writeFileSync(join(gitWorkDir, deletedName), "before"); + execFileSync("git", ["add", "--", modifiedName, deletedName], { cwd: gitWorkDir }); + execFileSync( + "git", + [ + "-c", + "user.name=SDK Test", + "-c", + "user.email=sdk-test@example.test", + "commit", + "--quiet", + "-m", + "Create workspace diff fixture", + ], + { cwd: gitWorkDir } + ); + writeFileSync(join(gitWorkDir, modifiedName), "after"); + unlinkSync(join(gitWorkDir, deletedName)); + const gitClient = createClient({ workingDirectory: gitWorkDir }); try { - const result = await session.rpc.workspaces.diff({ mode }); - expect(result.requestedMode).toBe(mode); - expect(["session", "unstaged", "branch"]).toContain(result.mode); - expect(Array.isArray(result.changes)).toBe(true); - for (const change of result.changes) { - expect(change.path.trim()).toBeTruthy(); - expect(["added", "modified", "deleted", "renamed"]).toContain( - change.changeType - ); - expect(typeof change.diff).toBe("string"); + const session = await gitClient.createSession({ + onPermissionRequest: approveAll, + }); + try { + const result = await session.rpc.workspaces.diff({ mode }); + expect(result.requestedMode).toBe(mode); + if (mode === "unstaged") { + expect(result.mode).toBe("unstaged"); + expect(result.isFallback).toBe(false); + expect(result.unavailableReason).toBeUndefined(); + } else { + expect([mode, "unstaged"]).toContain(result.mode); + expect(result.isFallback).toBe( + result.mode === "unstaged" || + result.unavailableReason === "session-busy" + ); + expect(result.unavailableReason !== undefined).toBe( + mode === "session" && result.isFallback + ); + } + expect(Array.isArray(result.changes)).toBe(true); + if (result.mode === "session") { + expect(result.changes).toEqual([]); + } else { + expect( + result.changes.find((change) => change.path.endsWith(modifiedName)) + ?.changeType + ).toBe("modified"); + expect( + result.changes.find((change) => change.path.endsWith(deletedName)) + ?.changeType + ).toBe("deleted"); + } + for (const change of result.changes) { + expect(change.path.trim()).toBeTruthy(); + expect(["added", "modified", "deleted", "renamed"]).toContain( + change.changeType + ); + expect(typeof change.diff).toBe("string"); + } + } finally { + await session.disconnect(); } } finally { - await session.disconnect(); + await gitClient.stop(); } } ); diff --git a/nodejs/test/e2e/runtime_host.e2e.test.ts b/nodejs/test/e2e/runtime_host.e2e.test.ts index 2a2f72d52e..b1b8a1d9d2 100644 --- a/nodejs/test/e2e/runtime_host.e2e.test.ts +++ b/nodejs/test/e2e/runtime_host.e2e.test.ts @@ -70,6 +70,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { it("preserves MCP Apps advertisement and channel access on callback-backed attachment", async () => { let original: CopilotSession | undefined; await using host = await owner.startAhpHost({ + localServer: {}, createSession: async ({ config }) => { expect(config.enableMcpApps).toBe(true); original = await owner.createSession({ @@ -153,6 +154,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { const released = vi.fn(); let original: CopilotSession | undefined; await using host = await owner.startAhpHost({ + localServer: {}, createSession: async ({ config, signal }) => { expect(signal.aborted).toBe(false); original = await owner.createSession({ @@ -228,6 +230,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { ); let original: CopilotSession | undefined; await using host = await owner.startAhpHost({ + localServer: {}, createSession: async ({ config }) => { original = await owner.createSession({ ...config, @@ -328,6 +331,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { const tool = vi.fn(({ seed }: { seed: string }) => `MAGIC_${seed}_42`); let original: CopilotSession | undefined; await using host = await owner.startAhpHost({ + localServer: {}, createSession: async ({ config }) => { original = await owner.createSession({ ...config, @@ -388,6 +392,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { const released = vi.fn(); let original: CopilotSession | undefined; await using host = await owner.startAhpHost({ + localServer: {}, createSession: async ({ config }) => { original = await owner.createSession({ ...config, @@ -447,6 +452,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { return original; }); const first = await firstOwner.startAhpHost({ + localServer: {}, createSession: create, onSessionReleased: released, }); @@ -509,6 +515,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { ); const resumedReleased = vi.fn(); const replacement = await resumedOwner.startAhpHost({ + localServer: {}, createSession: createAgain, resumeSession: resume, onSessionReleased: resumedReleased, @@ -608,6 +615,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { let sessionUri = ""; let sessionId = ""; await using first = await owner.startAhpHost({ + localServer: {}, createSession: async ({ config }) => { original = await owner.createSession({ ...config, @@ -658,6 +666,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { ); const resumedReleased = vi.fn(); await using replacement = await owner.startAhpHost({ + localServer: {}, createSession: createAgain, resumeSession: resume, onSessionReleased: resumedReleased, @@ -681,6 +690,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { const retainedResume = vi.fn(async () => resumedOriginal!); const retainedReleased = vi.fn(); await using retainedHost = await owner.startAhpHost({ + localServer: {}, resumeSession: retainedResume, onSessionReleased: retainedReleased, }); @@ -724,6 +734,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { }); const released = vi.fn(); await using host = await owner.startAhpHost({ + localServer: {}, createSession: factory, resumeSession: factory, onSessionReleased: released, @@ -842,13 +853,15 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { it("disposes listener and client without closing the runtime or owner session", async () => { await using session = await owner.createSession({ onPermissionRequest: approveAll }); const observed = exitObserver(); - await using host = await owner.startAhpHost({ onExit: observed.onExit }); + await using host = await owner.startAhpHost({ localServer: {}, onExit: observed.onExit }); const ahp = await connectAhp(host); try { await ahp.client.ping(); await assertRuntimeListener(host, runtimeDetails().pid, artifacts); - expect(new URL(host.url).hostname).toBe("127.0.0.1"); - expect(Number(new URL(host.url).port)).toBeGreaterThan(0); + expect(host.url).toBeDefined(); + expect(host.environmentId).toBeUndefined(); + expect(new URL(host.url!).hostname).toBe("127.0.0.1"); + expect(Number(new URL(host.url!).port)).toBeGreaterThan(0); expect(host.token?.length).toBeGreaterThan(0); await Promise.all([ owner.rpc.host.dispose({ hostId: host.hostId }), @@ -877,7 +890,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { const registerFromOwner = () => owner.rpc.secrets.addFilterValues({ values: ["ordinary-sdk-filter-sentinel"] }); await expect(registerFromOwner()).rejects.toThrow("COPILOT_ENABLE_SECRET_FILTERING"); - await using host = await owner.startAhpHost(); + await using host = await owner.startAhpHost({ localServer: {} }); const ahp = await connectAhp(host); try { const session = await createAhpSession(ahp, ctx.workDir, ctx.env.GITHUB_TOKEN); @@ -896,7 +909,9 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { const address = blocker.address(); if (!address || typeof address === "string") throw new Error("Missing blocker address"); try { - await expect(owner.startAhpHost({ port: address.port })).rejects.toThrow(); + await expect( + owner.startAhpHost({ localServer: { port: address.port } }) + ).rejects.toThrow(); await expect(session.getEvents()).resolves.toEqual(expect.any(Array)); } finally { await new Promise((resolve, reject) => @@ -905,7 +920,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { } // There is no host process to kill: listener recovery is not process isolation. await using host = await owner.startAhpHost({ - port: address.port, + localServer: { port: address.port }, onExit: observed.onExit, }); const ahp = await connectAhp(host); @@ -925,8 +940,10 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { it("honors listener endpoints and supplied tokens through framed RPC", async () => { const token = `ahp-e2e-${randomUUID()}`; - const ephemeral = await owner.startAhpHost({ hostname: "127.0.0.1", port: 0, token }); - const port = Number(new URL(ephemeral.url).port); + const ephemeral = await owner.startAhpHost({ + localServer: { hostname: "127.0.0.1", port: 0, token }, + }); + const port = Number(new URL(ephemeral.url!).port); expect(port).toBeGreaterThan(0); expect(ephemeral.token).toBe(token); const initialAhp = await connectAhp(ephemeral); @@ -939,12 +956,14 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { for (const hostname of ["0.0.0.0", "localhost", "::1"]) { await using host = await owner.startAhpHost({ - hostname, - port: hostname === "0.0.0.0" ? port : 0, - token, - requireConnectionToken: true, + localServer: { + hostname, + port: hostname === "0.0.0.0" ? port : 0, + token, + requireConnectionToken: true, + }, }); - const bound = new URL(host.url); + const bound = new URL(host.url!); expect(Number(bound.port)).toBeGreaterThan(0); if (hostname === "0.0.0.0") { expect(bound.hostname).toBe("0.0.0.0"); @@ -954,7 +973,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { } else { expect(["127.0.0.1", "[::1]"]).toContain(bound.hostname); } - const reachable = new URL(host.url); + const reachable = new URL(host.url!); if (reachable.hostname === "0.0.0.0") reachable.hostname = "127.0.0.1"; await expect(connectAhp({ url: reachable.href, token: undefined })).rejects.toThrow(); await expect( @@ -973,6 +992,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { }); it("disables only connection-token auth and rejects invalid listener options through direct RPC", async () => { + await expect(owner.rpc.host.start({ hostId: randomUUID() })).rejects.toThrow(); for (const options of [ { port: -1 }, { port: 65536 }, @@ -981,10 +1001,12 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { { token: "supplied", requireConnectionToken: false }, ]) { await expect( - owner.rpc.host.start({ hostId: randomUUID(), ...options }) + owner.rpc.host.start({ hostId: randomUUID(), localServer: options }) ).rejects.toThrow(); } - await using host = await owner.startAhpHost({ requireConnectionToken: false }); + await using host = await owner.startAhpHost({ + localServer: { requireConnectionToken: false }, + }); expect(host.token).toBeUndefined(); const ahp = await connectAhp(host); try { @@ -1029,7 +1051,10 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { }); try { const observed = exitObserver(); - const abandonedHost = await otherOwner.startAhpHost({ onExit: observed.onExit }); + const abandonedHost = await otherOwner.startAhpHost({ + localServer: {}, + onExit: observed.onExit, + }); const abandonedAhp = await connectAhp(abandonedHost); try { const session = await createAhpSession( @@ -1045,7 +1070,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { ); expect(response.text).toContain("4"); await assertRuntimeListener(abandonedHost, runtimeDetails().pid, artifacts); - await expect(owner.startAhpHost()).rejects.toThrow( + await expect(owner.startAhpHost({ localServer: {} })).rejects.toThrow( /catalog (?:is )?already in use/ ); await expect( @@ -1063,7 +1088,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { await assertHostStopped(abandonedHost, abandonedAhp); await expect(survivingSession.getEvents()).resolves.toEqual(expect.any(Array)); expect(observed.exits).toHaveLength(1); - await using replacement = await owner.startAhpHost(); + await using replacement = await owner.startAhpHost({ localServer: {} }); const replacementAhp = await connectAhp(replacement); try { await resumeAhp(replacementAhp, session.sessionUri, survivingSession.sessionId); @@ -1092,7 +1117,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { onPermissionRequest: approveAll, streaming: true, }); - await using host = await owner.startAhpHost(); + await using host = await owner.startAhpHost({ localServer: {} }); const ahp = await connectAhp(host); try { const session = await createAhpSession(ahp, ctx.workDir, ctx.env.GITHUB_TOKEN); @@ -1124,7 +1149,10 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { ).toBe(true); const observed = exitObserver(); - await using replacement = await owner.startAhpHost({ onExit: observed.onExit }); + await using replacement = await owner.startAhpHost({ + localServer: {}, + onExit: observed.onExit, + }); const replacementAhp = await connectAhp(replacement); try { const resumed = await resumeAhp( @@ -1141,7 +1169,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { } finally { await replacementAhp.client.shutdown(); } - await using recovered = await owner.startAhpHost(); + await using recovered = await owner.startAhpHost({ localServer: {} }); const recoveredAhp = await connectAhp(recovered); try { const resumed = await resumeAhp( @@ -1167,7 +1195,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { const second = ctx.createClient({ baseDirectory }); let sessionUri: string; try { - await using host = await first.startAhpHost(); + await using host = await first.startAhpHost({ localServer: {} }); const ahp = await connectAhp(host); try { const runtime = (first as unknown as { cliProcess: ChildProcess }).cliProcess; @@ -1186,7 +1214,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { await using ordinary = await second.createSession({ onPermissionRequest: approveAll, }); - await expect(second.startAhpHost()).rejects.toThrow( + await expect(second.startAhpHost({ localServer: {} })).rejects.toThrow( /catalog (?:is )?already in use/ ); await expect(ordinary.getEvents()).resolves.toEqual(expect.any(Array)); @@ -1203,7 +1231,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { const restarted = ctx.createClient({ baseDirectory }); try { - await using host = await restarted.startAhpHost(); + await using host = await restarted.startAhpHost({ localServer: {} }); const ahp = await connectAhp(host); try { const resumed = await resumeAhp(ahp, sessionUri!); @@ -1218,7 +1246,7 @@ describe.skipIf(!enabled)("Runtime-supervised AHP host", async () => { it("gracefully shuts down the runtime with an attached AHP session", async () => { const observed = exitObserver(); - const host = await owner.startAhpHost({ onExit: observed.onExit }); + const host = await owner.startAhpHost({ localServer: {}, onExit: observed.onExit }); const ahp = await connectAhp(host); const runtimePid = runtimeDetails().pid; try { diff --git a/nodejs/test/e2e/sandbox_devices.e2e.test.ts b/nodejs/test/e2e/sandbox_devices.e2e.test.ts new file mode 100644 index 0000000000..173289e71a --- /dev/null +++ b/nodejs/test/e2e/sandbox_devices.e2e.test.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { describe, it } from "vitest"; +import type { PermissionRequest } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +const TEST_NAME = "does not offer a sandbox bypass for standard device paths"; + +describe("Sandbox devices", async () => { + if (process.platform !== "darwin") { + // The SDK sandbox suite currently has a backend only on macOS. + it.skip(TEST_NAME, () => undefined); + return; + } + + const { copilotClient: client, workDir } = await createSdkTestContext({ + copilotClientOptions: { + env: { COPILOT_CLI_ENABLED_FEATURE_FLAGS: "SANDBOX" }, + }, + }); + + it(TEST_NAME, { timeout: 180_000 }, async ({ expect }) => { + const denialText = "cat: /dev/stdin: Operation not permitted\n"; + await writeFile(join(workDir, "denial.txt"), denialText); + const bypassRequests: PermissionRequest[] = []; + const shellResults: string[] = []; + const shellCalls = new Set(); + const session = await client.createSession({ + onPermissionRequest: (request) => { + if ("requestSandboxBypass" in request && request.requestSandboxBypass) { + bypassRequests.push(request); + return { kind: "reject" }; + } + return { kind: "approve-once" }; + }, + }); + try { + const update = await session.rpc.options.update({ + sandboxConfig: { + enabled: true, + allowBypass: true, + addCurrentWorkingDirectory: true, + }, + }); + expect(update.success).toBe(true); + session.on((event) => { + if (event.type === "tool.execution_start" && event.data.toolName === "bash") { + shellCalls.add(event.data.toolCallId); + } else if ( + event.type === "tool.execution_complete" && + shellCalls.has(event.data.toolCallId) && + event.data.success + ) { + shellResults.push(event.data.result?.content ?? ""); + } + }); + + const message = await session.sendAndWait( + { + prompt: + "Use the bash tool exactly once to run this exact command: " + + "`head -c 16 /dev/urandom > entropy.bin && " + + "cat /dev/stdin < denial.txt > copy.txt && cat copy.txt`. " + + "Do not request a sandbox bypass. After the tool returns, " + + "reply with exactly DEVICE_COMMAND_COMPLETE.", + }, + 120_000 + ); + + expect(message?.data.content).toContain("DEVICE_COMMAND_COMPLETE"); + expect(shellCalls.size).toBe(1); + expect(shellResults).toHaveLength(1); + expect(shellResults[0]).toContain(denialText.trim()); + expect(shellResults[0]).toContain("exit code 0"); + expect(await readFile(join(workDir, "entropy.bin"))).toHaveLength(16); + expect(await readFile(join(workDir, "copy.txt"), "utf8")).toBe(denialText); + expect(bypassRequests).toEqual([]); + } finally { + await session.disconnect(); + } + }); +}); diff --git a/nodejs/test/e2e/sandbox_filesystem.e2e.test.ts b/nodejs/test/e2e/sandbox_filesystem.e2e.test.ts new file mode 100644 index 0000000000..80a7baf04a --- /dev/null +++ b/nodejs/test/e2e/sandbox_filesystem.e2e.test.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { existsSync } from "node:fs"; +import { readFile, stat } from "node:fs/promises"; +import { join } from "node:path"; +import { describe, it } from "vitest"; +import { approveAll } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +const TEST_NAME = "creates missing denied directories on the host before a sandboxed shell runs"; + +describe("Sandbox filesystem", async () => { + if (process.platform !== "darwin") { + // The SDK sandbox suite currently has a backend only on macOS. + it.skip(TEST_NAME, () => undefined); + return; + } + + const { copilotClient: client, workDir } = await createSdkTestContext({ + copilotClientOptions: { + env: { COPILOT_CLI_ENABLED_FEATURE_FLAGS: "SANDBOX" }, + }, + }); + + it(TEST_NAME, { timeout: 120_000 }, async ({ expect }) => { + const denied = join(workDir, "vault", ".env"); + expect(existsSync(denied)).toBe(false); + const session = await client.createSession({ onPermissionRequest: approveAll }); + try { + const update = await session.rpc.options.update({ + sandboxConfig: { + enabled: true, + allowBypass: false, + addCurrentWorkingDirectory: true, + userPolicy: { + filesystem: { deniedPaths: ["./vault/.env"] }, + network: { allowOutbound: false }, + }, + }, + }); + expect(update.success).toBe(true); + const result = await session.rpc.shell.executeUserRequested({ + requestId: "denied-directory", + command: + "echo control > control.txt && " + + "if echo blocked > vault/.env/probe; then echo SANDBOX_WROTE; " + + "else echo SANDBOX_DENIED; fi", + }); + expect(result.success, result.output).toBe(true); + expect(result.exitCode, result.output).toBe(0); + expect(result.output).toContain("SANDBOX_DENIED"); + expect(result.output).not.toContain("SANDBOX_WROTE"); + expect((await stat(denied)).isDirectory()).toBe(true); + expect(existsSync(join(denied, "probe"))).toBe(false); + expect((await readFile(join(workDir, "control.txt"), "utf8")).trim()).toBe("control"); + } finally { + await session.disconnect(); + } + }); +}); diff --git a/nodejs/test/e2e/sandbox_gh_config.e2e.test.ts b/nodejs/test/e2e/sandbox_gh_config.e2e.test.ts new file mode 100644 index 0000000000..69542c5d9f --- /dev/null +++ b/nodejs/test/e2e/sandbox_gh_config.e2e.test.ts @@ -0,0 +1,138 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { execFileSync } from "node:child_process"; +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { describe, it } from "vitest"; +import { approveAll } from "../../src/index.js"; +import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js"; + +const TEST_NAME = + "preserves gh settings and aliases in sandboxed scripts without copying credentials"; + +describe("Sandbox gh configuration", async () => { + if (process.platform !== "darwin") { + // The SDK sandbox suite currently has a backend only on macOS. + it.skip(TEST_NAME, () => undefined); + return; + } + + const { + copilotClient: client, + homeDir, + workDir, + } = await createSdkTestContext({ + copilotClientOptions: { + gitHubToken: DEFAULT_GITHUB_TOKEN, + env: { + COPILOT_CLI_ENABLED_FEATURE_FLAGS: "SANDBOX", + GH_TOKEN: DEFAULT_GITHUB_TOKEN, + GITHUB_TOKEN: DEFAULT_GITHUB_TOKEN, + GITHUB_COPILOT_GITHUB_TOKEN: DEFAULT_GITHUB_TOKEN, + GIT_CONFIG_GLOBAL: "/dev/null", + GIT_CONFIG_NOSYSTEM: "1", + }, + }, + }); + + it(TEST_NAME, { timeout: 120_000 }, async ({ expect }) => { + execFileSync("git", ["init", "--quiet"], { cwd: workDir, stdio: "pipe" }); + const token = `ghp_${"a".repeat(36)}`; + const source = join(homeDir, "config.yml"); + const config = (editor: string) => + [ + 'version: "1"', + `editor: ${editor}`, + "pager:", + "browser:", + "http_unix_socket:", + "aliases:", + ` sandbox-check: ${JSON.stringify( + `!case "$GITHUB_TOKEN" in copilot_mask_*) printf '%s\\n' GH_ALIAS_AVAILABLE ;; *) exit 73 ;; esac` + )}`, + " token: auth token", + " who: auth token", + ` embedded-secret: ${JSON.stringify(`!echo ${token}`)}`, + "hosts:", + " github.com:", + ` oauth_token: ${token}`, + "", + ].join("\n"); + await writeFile(source, config("sandbox-editor-one")); + await writeFile(join(homeDir, "hosts.yml"), `github.com:\n oauth_token: ${token}\n`); + await writeFile( + join(workDir, "release.sh"), + [ + "set -eu", + 'case "$GH_TOKEN" in copilot_mask_*) ;; *) exit 71 ;; esac', + 'test ! -e "$GH_CONFIG_DIR/hosts.yml"', + `if cat ${JSON.stringify(join(homeDir, "hosts.yml"))} >/dev/null 2>&1; then exit 72; fi`, + 'cp "$GH_CONFIG_DIR/config.yml" copied-config.yml', + 'printf "%s" "$GH_CONFIG_DIR" > copied-config-dir.txt', + "gh sandbox-check", + 'test -z "$(gh config get http_unix_socket)"', + "gh alias list", + "gh config get editor", + "printf '%s\\n' GH_SCRIPT_COMPLETE", + "", + ].join("\n") + ); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + try { + const update = await session.rpc.options.update({ + sandboxConfig: { + enabled: true, + addCurrentWorkingDirectory: true, + auth: { git: false, gh: true }, + userPolicy: { + filesystem: { deniedPaths: [homeDir] }, + network: { allowOutbound: false }, + }, + }, + }); + expect(update.success).toBe(true); + + let previousDirectory: string | undefined; + for (const editor of ["sandbox-editor-one", "sandbox-editor-two"]) { + const original = config(editor); + await writeFile(source, original); + const result = await session.rpc.shell.executeUserRequested({ + requestId: editor, + command: "sh release.sh", + }); + expect(result.success, result.output).toBe(true); + expect(result.exitCode, result.output).toBe(0); + expect(result.output).toContain("GH_ALIAS_AVAILABLE"); + expect(result.output).toContain("token: auth token"); + expect(result.output).toContain("who: auth token"); + expect(result.output).toContain(editor); + expect(result.output).toContain("GH_SCRIPT_COMPLETE"); + + // Read raw child-written bytes, not secret-filtered tool output. + const copied = await readFile(join(workDir, "copied-config.yml"), "utf8"); + expect(copied).not.toContain(token); + expect(copied).not.toContain("oauth_token"); + expect(copied).not.toContain("hosts:"); + expect(copied).toContain("sandbox-check"); + expect(await readFile(source, "utf8")).toBe(original); + const directory = await readFile(join(workDir, "copied-config-dir.txt"), "utf8"); + expect(directory).not.toBe(homeDir); + expect(directory).not.toBe(previousDirectory); + previousDirectory = directory; + + const direct = await session.rpc.shell.executeUserRequested({ + requestId: `${editor}-direct-alias`, + command: "gh sandbox-check", + }); + expect(direct.success, direct.output).toBe(true); + expect(direct.exitCode, direct.output).toBe(0); + expect(direct.output).toContain("GH_ALIAS_AVAILABLE"); + } + } finally { + await session.disconnect(); + } + }); +}); diff --git a/nodejs/test/e2e/sandbox_git_scripts.e2e.test.ts b/nodejs/test/e2e/sandbox_git_scripts.e2e.test.ts new file mode 100644 index 0000000000..89499fc687 --- /dev/null +++ b/nodejs/test/e2e/sandbox_git_scripts.e2e.test.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { execFileSync } from "node:child_process"; +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { describe, it } from "vitest"; +import { approveAll } from "../../src/index.js"; +import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js"; + +const TEST_NAME = "gives Python Git subprocesses masked credentials and observes auth changes"; + +describe("Sandbox Git scripts", async () => { + if (process.platform !== "darwin") { + // The SDK sandbox suite currently has a backend only on macOS. + it.skip(TEST_NAME, () => undefined); + return; + } + + const { copilotClient: client, workDir } = await createSdkTestContext({ + copilotClientOptions: { + gitHubToken: DEFAULT_GITHUB_TOKEN, + env: { + COPILOT_CLI_ENABLED_FEATURE_FLAGS: "SANDBOX", + GH_TOKEN: DEFAULT_GITHUB_TOKEN, + GITHUB_TOKEN: DEFAULT_GITHUB_TOKEN, + GITHUB_COPILOT_GITHUB_TOKEN: DEFAULT_GITHUB_TOKEN, + GIT_CONFIG_GLOBAL: "/dev/null", + GIT_CONFIG_NOSYSTEM: "1", + GIT_CONFIG_COUNT: "0", + }, + }, + }); + + it(TEST_NAME, { timeout: 120_000 }, async ({ expect }) => { + execFileSync("git", ["init", "--quiet"], { cwd: workDir, stdio: "pipe" }); + execFileSync( + "git", + ["remote", "add", "origin", "git@github.com:example/private-repo.git"], + { cwd: workDir, stdio: "pipe" } + ); + await writeFile( + join(workDir, "inspect_git.py"), + [ + "import pathlib, subprocess", + "config = subprocess.check_output(['git', 'config', '--list'], text=True)", + "pathlib.Path('child-git-config.txt').write_text(config)", + "remote = subprocess.check_output(['git', 'remote', 'get-url', 'origin'], text=True)", + "pathlib.Path('child-git-remote.txt').write_text(remote)", + "print('GIT_SCRIPT_COMPLETE')", + "", + ].join("\n") + ); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + try { + let previousHeader: string | undefined; + for (const [index, enabled] of [true, false, true].entries()) { + const update = await session.rpc.options.update({ + sandboxConfig: { + enabled: true, + addCurrentWorkingDirectory: true, + auth: { git: enabled, gh: false }, + userPolicy: { network: { allowOutbound: false } }, + }, + }); + expect(update.success).toBe(true); + const result = await session.rpc.shell.executeUserRequested({ + requestId: `git-script-${index}`, + command: "python3 inspect_git.py", + }); + expect(result.success, result.output).toBe(true); + expect(result.exitCode, result.output).toBe(0); + expect(result.output).toContain("GIT_SCRIPT_COMPLETE"); + + // Inspect raw child-written bytes, not redacted tool output. + const config = await readFile(join(workDir, "child-git-config.txt"), "utf8"); + expect(config).not.toContain(DEFAULT_GITHUB_TOKEN); + expect(config).not.toContain( + Buffer.from(`x-access-token:${DEFAULT_GITHUB_TOKEN}`).toString("base64") + ); + const headers = config + .split("\n") + .filter((line) => + line.startsWith("http.https://github.com/.extraheader=Authorization: ") + ); + if (enabled) { + expect(headers).toHaveLength(1); + expect(headers[0]).toMatch(/=Authorization: copilot_mask_/); + expect(headers[0]).not.toBe(previousHeader); + previousHeader = headers[0]; + } else { + expect(headers).toEqual([]); + } + expect((await readFile(join(workDir, "child-git-remote.txt"), "utf8")).trim()).toBe( + enabled + ? "https://github.com/example/private-repo.git" + : "git@github.com:example/private-repo.git" + ); + } + } finally { + await session.disconnect(); + } + }); +}); diff --git a/nodejs/test/e2e/scenario_testing_control_state.e2e.test.ts b/nodejs/test/e2e/scenario_testing_control_state.e2e.test.ts new file mode 100644 index 0000000000..071f6df2b4 --- /dev/null +++ b/nodejs/test/e2e/scenario_testing_control_state.e2e.test.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +import { approveAll } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +describe("Scenario control state RPC", async () => { + const { copilotClient: client } = await createSdkTestContext(); + + it("should compose mode name plan client metadata and objective state", async () => { + const session = await client.createSession({ onPermissionRequest: approveAll }); + try { + const name = "Scenario control state"; + const plan = "# Scenario plan\n- Verify control state"; + const objective = '{"objective":"VERIFY_SCENARIO_CONTROL","status":"active"}'; + await session.rpc.mode.set({ mode: "plan" }); + await session.rpc.name.set({ name }); + await session.rpc.plan.update({ content: plan }); + const metadata = await session.rpc.metadata.updateClientMetadata({ + set: { + "scenario-client/control-mode": "plan", + "scenario-client/objective": "VERIFY_SCENARIO_CONTROL", + }, + }); + expect( + (await session.rpc.workspaces.writeAutopilotObjective({ content: objective })) + .operation + ).toBe("create"); + expect((await session.rpc.workspaces.autopilotObjectiveExists()).exists).toBe(true); + expect((await session.rpc.workspaces.readAutopilotObjective()).content).toBe(objective); + expect((await session.rpc.plan.read()).content).toBe(plan); + expect((await session.rpc.name.get()).name).toBe(name); + expect(metadata["scenario-client/objective"]).toBe("VERIFY_SCENARIO_CONTROL"); + const snapshot = await session.rpc.metadata.snapshot(); + expect(snapshot.sessionId).toBe(session.sessionId); + expect(snapshot.currentMode).toBe("plan"); + expect(snapshot.initialName ?? null).toBeNull(); + expect((await session.rpc.workspaces.deleteAutopilotObjective()).deleted).toBe(true); + expect((await session.rpc.workspaces.autopilotObjectiveExists()).exists).toBe(false); + } finally { + await session.disconnect(); + } + }); +}); diff --git a/nodejs/test/e2e/scenario_testing_permissions.e2e.test.ts b/nodejs/test/e2e/scenario_testing_permissions.e2e.test.ts new file mode 100644 index 0000000000..577c1bef58 --- /dev/null +++ b/nodejs/test/e2e/scenario_testing_permissions.e2e.test.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +import { approveAll } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +describe("Scenario permission mode RPC", async () => { + const { copilotClient: client } = await createSdkTestContext(); + + it.each([ + ["assisted", "gpt-5.5"], + ["allow-all", undefined], + ] as const)("sets, resets and reads authoritative %s permission mode", async (mode, model) => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + featureFlags: { AUTO_APPROVAL: true }, + }); + try { + expect((await session.rpc.permissions.getMode()).mode).toBe("manual"); + const set = await session.rpc.permissions.setMode({ + mode, + assistedApprovalModel: model, + source: "rpc", + }); + expect(set).toMatchObject({ success: true, mode }); + expect((await session.rpc.permissions.getMode()).mode).toBe(mode); + expect( + await session.rpc.permissions.setMode({ mode: "manual", source: "rpc" }) + ).toMatchObject({ success: true, mode: "manual" }); + expect((await session.rpc.permissions.getMode()).mode).toBe("manual"); + } finally { + await session.disconnect(); + } + }); +}); diff --git a/nodejs/test/e2e/scenario_testing_persistence.e2e.test.ts b/nodejs/test/e2e/scenario_testing_persistence.e2e.test.ts index 6d776ac57c..5b179a77ac 100644 --- a/nodejs/test/e2e/scenario_testing_persistence.e2e.test.ts +++ b/nodejs/test/e2e/scenario_testing_persistence.e2e.test.ts @@ -33,4 +33,54 @@ describe("Scenario testing persistence", async () => { await session.disconnect(); } }); + + it("should page persisted events backward without resuming", async () => { + const firstPrompt = "Reply with exactly PERSISTED_SCENARIO_FIRST."; + const secondPrompt = "Reply with exactly PERSISTED_SCENARIO_SECOND."; + const session = await client.createSession({ onPermissionRequest: approveAll }); + const sessionId = session.sessionId; + try { + expect((await session.sendAndWait({ prompt: firstPrompt }))?.data.content).toContain( + "PERSISTED_SCENARIO_FIRST" + ); + expect((await session.sendAndWait({ prompt: secondPrompt }))?.data.content).toContain( + "PERSISTED_SCENARIO_SECOND" + ); + await client.rpc.sessions.save({ sessionId }); + } finally { + await session.disconnect(); + } + + const pageSizes = [1, 3]; + for (const max of pageSizes) { + const events: Array<{ id: string; type: string; data: unknown }> = []; + let page = await client.rpc.sessions.readPersistedEvents({ + sessionId, + max, + direction: "backward", + }); + for (;;) { + expect(page.cursorStatus).toBe("ok"); + events.push(...page.events); + if (!page.hasMore) { + break; + } + expect(page.cursor).toBeTruthy(); + page = await client.rpc.sessions.readPersistedEvents({ + sessionId, + max, + cursor: page.cursor, + }); + } + expect(new Set(events.map((event) => event.id)).size).toBe(events.length); + const userMessages = events + .filter((event) => event.type === "user.message") + .map((event) => (event.data as { content: string }).content); + expect(userMessages).toContain(firstPrompt); + expect(userMessages).toContain(secondPrompt); + expect(userMessages.indexOf(secondPrompt)).toBeLessThan( + userMessages.indexOf(firstPrompt) + ); + } + }, 90_000); }); diff --git a/nodejs/test/e2e/scenario_testing_server_control.e2e.test.ts b/nodejs/test/e2e/scenario_testing_server_control.e2e.test.ts new file mode 100644 index 0000000000..0bef912e0e --- /dev/null +++ b/nodejs/test/e2e/scenario_testing_server_control.e2e.test.ts @@ -0,0 +1,296 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { RuntimeConnection } from "../../src/index.js"; +import type { CopilotClient } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +const FAKE_CLI = ` +const fs = require("node:fs"); +const requests = []; +const capture = process.argv[process.argv.indexOf("--capture-file") + 1]; +let queue = [], steeringMessages = [], nextId = 0, buffer = Buffer.alloc(0); +function reply(id, result) { + const body = JSON.stringify({jsonrpc:"2.0",id,result}); + process.stdout.write("Content-Length: " + Buffer.byteLength(body) + "\\r\\n\\r\\n" + body); +} +function handle(request) { + if (request.id === undefined) return; + const {id,method,params={}} = request; + requests.push({method,params}); + fs.writeFileSync(capture, JSON.stringify(requests)); + if (method === "connect") return reply(id, {ok:true,protocolVersion:3,version:"scenario-control"}); + if (method === "session.create") return reply(id, {sessionId:params.sessionId || "scenario-control",workspacePath:null,capabilities:{supportsStreaming:true}}); + if (method === "session.detach") return reply(id, {success:true}); + if (method === "catalog.search") return reply(id, {kind:"succeeded", candidates:[], searchId:"scenario-search", truncated:false, negotiated:{runtimeProtocolVersion:params.contract.protocolVersion,grantedCapabilities:params.contract.requiredCapabilities}}); + if (method === "session.workflow.listRuns") return reply(id, {runs:[{runId:"workflow-run-1",workflowName:"scenario-workflow",status:"running",revision:4}],oldestSeq:7,newestSeq:7,hasMoreNewer:false}); + if (method === "session.workflow.getRunDetail") return reply(id, {runId:"workflow-run-1",workflowName:"scenario-workflow",status:"running",revision:4}); + if (method === "session.workflow.getRunProgress") return reply(id, {records:[{seq:12,phaseId:"verify",kind:"log",text:"Validation complete"}],oldestSeq:12,newestSeq:12,revision:4,hasMoreOlder:false,hasMoreNewer:false}); + if (method === "session.workflow.cancel") return reply(id, {runId:params.runId,status:"cancelled",reason:"cancelled by user",attempt:1}); + if (method === "session.autopilotObjective.getState") return reply(id, {state:{id:17,objective:"Ship the scenario.",status:"active",turnCount:3,creditCountNanoAiu:"1250000000",creditLimit:{credits:5,creditsUsed:1.25,creditsUsedNanoAiu:"1250000000"}}}); + if (method === "session.remote.enable") return reply(id, {url:"https://example.test/sessions/"+params.sessionId,remoteSteerable:params.mode==="on"}); + if (method === "session.queue.insertAt") { + const item = {id:"queue-"+(++nextId),messageId:"message-"+nextId,kind:"message",prompt:params.message.prompt,displayText:params.message.displayPrompt||params.message.prompt,agentMode:params.message.agentMode||"interactive"}; + queue.splice(params.position,0,item); return reply(id,{id:item.id}); + } + if (method === "session.queue.pendingItems") return reply(id,{items:queue.map(({prompt,...item})=>item),steeringMessages,inFlightSteeringCount:0}); + if (method === "session.queue.updateText") { + const item=queue.find(x=>x.id===params.id); + if(item){item.prompt=params.prompt;item.displayText=params.displayPrompt||params.prompt;} + return reply(id,{updated:!!item}); + } + if (method === "session.queue.duplicateAt") { + const index=queue.findIndex(x=>x.id===params.id), id2="queue-"+(++nextId); + if(index>=0)queue.splice(index+1,0,{...queue[index],id:id2,messageId:"message-"+nextId}); + return reply(id,{id:id2}); + } + if (method === "session.queue.moveItem") { + const index=queue.findIndex(x=>x.id===params.id); + if(index<0)return reply(id,{changed:false}); + queue.splice(params.toPosition,0,...queue.splice(index,1)); + return reply(id,{changed:index!==params.toPosition}); + } + if (method === "session.queue.removeAt" || method === "session.queue.sendNow") { + const index=queue.findIndex(x=>x.id===params.id); + if(index>=0){ + const [item]=queue.splice(index,1); + if(method.endsWith("sendNow"))steeringMessages.push(item.displayText); + } + return reply(id,method.endsWith("sendNow")?{steered:index>=0}:{removed:index>=0}); + } + reply(id,null); +} +process.stdin.on("data",chunk=>{ + buffer=Buffer.concat([buffer,chunk]); + for(;;){ + const end=buffer.indexOf("\\r\\n\\r\\n");if(end<0)break; + const match=/Content-Length:\\s*(\\d+)/i.exec(buffer.subarray(0,end).toString()); + if(!match)throw Error("Missing Content-Length"); + const offset=end+4,size=Number(match[1]);if(buffer.length { + const { createClient, workDir } = await createSdkTestContext(); + + async function withFakeCli( + action: ( + client: CopilotClient, + requests: () => Array<{ method: string; params: Record }> + ) => Promise + ) { + const cliPath = join(workDir, "scenario-control-cli.js"); + const capturePath = join(workDir, "scenario-control-requests.json"); + writeFileSync(cliPath, FAKE_CLI); + const client = createClient({ + connection: RuntimeConnection.forStdio({ + path: cliPath, + args: ["--capture-file", capturePath], + }), + useLoggedInUser: false, + }); + try { + await client.start(); + await action(client, () => JSON.parse(readFileSync(capturePath, "utf8"))); + } finally { + await client.forceStop(); + } + } + + it.each([ + ["all", ["mcp-server", "ai-skill"], ["mcp-server-card", "ai-skill-discovery"]], + ["mcp", ["mcp-server"], ["mcp-server-card"]], + ["skills", ["ai-skill"], ["ai-skill-discovery"]], + ] as const)( + "searches server catalog for %s category", + async (_category, kinds, capabilities) => { + await withFakeCli(async (client, requests) => { + const result = await client.rpc.catalog.search({ + contract: { protocolVersion: 3, requiredCapabilities: [...capabilities] }, + query: "scenario search", + limit: 50, + kinds: [...kinds], + }); + expect(result).toMatchObject({ + kind: "succeeded", + candidates: [], + searchId: "scenario-search", + negotiated: { runtimeProtocolVersion: 3, grantedCapabilities: capabilities }, + truncated: false, + }); + expect( + requests().find((request) => request.method === "catalog.search")?.params + ).toMatchObject({ + query: "scenario search", + limit: 50, + kinds, + contract: { protocolVersion: 3, requiredCapabilities: capabilities }, + }); + }); + } + ); + + it("should observe page and cancel factory run", async () => { + await withFakeCli(async (client, requests) => { + const session = await client.createSession({}); + const runs = await session.rpc.workflow.listRuns({ + afterSeq: 3, + beforeSeq: 20, + limit: 10, + }); + expect(runs).toMatchObject({ oldestSeq: 7, newestSeq: 7, hasMoreNewer: false }); + expect(runs.runs[0]).toMatchObject({ + runId: "workflow-run-1", + workflowName: "scenario-workflow", + status: "running", + }); + const detail = await session.rpc.workflow.getRunDetail({ runId: "workflow-run-1" }); + expect(detail).toMatchObject({ + runId: "workflow-run-1", + workflowName: "scenario-workflow", + status: "running", + revision: 4, + }); + const progress = await session.rpc.workflow.getRunProgress({ + runId: "workflow-run-1", + phaseId: "verify", + afterSeq: 5, + beforeSeq: 20, + limit: 25, + }); + expect(progress.records[0]).toMatchObject({ + seq: 12, + phaseId: "verify", + kind: "log", + text: "Validation complete", + }); + expect(await session.rpc.workflow.cancel({ runId: "workflow-run-1" })).toMatchObject({ + runId: "workflow-run-1", + status: "cancelled", + reason: "cancelled by user", + }); + expect( + requests().find((request) => request.method === "session.workflow.listRuns")?.params + ).toMatchObject({ + afterSeq: 3, + beforeSeq: 20, + limit: 10, + }); + expect( + requests().find((request) => request.method === "session.workflow.getRunProgress") + ?.params + ).toMatchObject({ + runId: "workflow-run-1", + phaseId: "verify", + afterSeq: 5, + beforeSeq: 20, + limit: 25, + }); + await session.disconnect(); + }); + }); + + it.each([ + ["on", true], + ["export", false], + ] as const)( + "should read autopilot state and enable remote mode %s", + async (mode, steerable) => { + await withFakeCli(async (client, requests) => { + const session = await client.createSession({}); + expect((await session.rpc.autopilotObjective.getState()).state).toMatchObject({ + id: 17, + objective: "Ship the scenario.", + status: "active", + turnCount: 3, + creditCountNanoAiu: "1250000000", + creditLimit: { + credits: 5, + creditsUsed: 1.25, + creditsUsedNanoAiu: "1250000000", + }, + }); + expect(await session.rpc.remote.enable({ mode })).toMatchObject({ + remoteSteerable: steerable, + url: `https://example.test/sessions/${session.sessionId}`, + }); + expect( + requests().find((request) => request.method === "session.remote.enable")?.params + ).toMatchObject({ + sessionId: session.sessionId, + mode, + }); + await session.disconnect(); + }); + } + ); + + it("should edit reorder duplicate remove and send queued items", async () => { + await withFakeCli(async (client, requests) => { + const session = await client.createSession({}); + await session.rpc.queue.setDrainPaused({ paused: true }); + const first = await session.rpc.queue.insertAt({ + position: 0, + message: { + prompt: "First hidden prompt", + displayPrompt: "First visible prompt", + agentMode: "interactive", + }, + }); + const second = await session.rpc.queue.insertAt({ + position: 1, + message: { + prompt: "Second hidden prompt", + displayPrompt: "Second visible prompt", + agentMode: "plan", + }, + }); + expect( + ( + await session.rpc.queue.updateText({ + id: first.id, + prompt: "Updated hidden prompt", + displayPrompt: "Updated visible prompt", + }) + ).updated + ).toBe(true); + const duplicate = await session.rpc.queue.duplicateAt({ id: first.id }); + expect(duplicate.id).not.toBe(first.id); + expect( + (await session.rpc.queue.moveItem({ id: second.id, toPosition: 0 })).changed + ).toBe(true); + const items = (await session.rpc.queue.pendingItems()).items; + expect(items.map((item) => item.id)).toEqual([second.id, first.id, duplicate.id]); + expect(items[1]).toMatchObject({ + displayText: "Updated visible prompt", + agentMode: "interactive", + }); + expect((await session.rpc.queue.sendNow({ id: second.id })).steered).toBe(true); + expect((await session.rpc.queue.pendingItems()).steeringMessages).toEqual([ + "Second visible prompt", + ]); + expect((await session.rpc.queue.removeAt({ id: duplicate.id })).removed).toBe(true); + expect((await session.rpc.queue.pendingItems()).items).toEqual([ + expect.objectContaining({ + id: first.id, + displayText: "Updated visible prompt", + }), + ]); + await session.rpc.queue.setDrainPaused({ paused: false }); + expect( + requests() + .filter((request) => request.method === "session.queue.setDrainPaused") + .map((request) => request.params.paused) + ).toEqual([true, false]); + await session.disconnect(); + }); + }); +}); diff --git a/nodejs/test/e2e/session.e2e.test.ts b/nodejs/test/e2e/session.e2e.test.ts index d9f1124291..10f3b49f8a 100644 --- a/nodejs/test/e2e/session.e2e.test.ts +++ b/nodejs/test/e2e/session.e2e.test.ts @@ -1,5 +1,5 @@ import { rm } from "fs/promises"; -import { describe, expect, it, onTestFinished, vi } from "vitest"; +import { describe, expect, it, onTestFailed, onTestFinished, vi } from "vitest"; import { ParsedHttpExchange } from "../../../test/harness/replayingCapiProxy.js"; import { CopilotClient, approveAll, defineTool, RuntimeConnection } from "../../src/index.js"; import { createSdkTestContext, DEFAULT_GITHUB_TOKEN, isCI } from "./harness/sdkTestContext.js"; @@ -99,7 +99,7 @@ describe("Sessions", () => { it("should recover marker after cold resume with explicit session id", async () => { const sessionId = `e2e-resume-${Date.now()}`; const marker = "MARKER-7f3ac21e"; - const firstClient = new CopilotClient({ + const firstClient = createClient({ workingDirectory: workDir, env, connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), @@ -127,7 +127,7 @@ describe("Sessions", () => { await session.disconnect(); await firstClient.stop(); - const secondClient = new CopilotClient({ + const secondClient = createClient({ workingDirectory: workDir, env, connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), @@ -241,6 +241,9 @@ describe("Sessions", () => { content: systemMessageSuffix, }, }); + await session.rpc.tools.updateSubagentSettings({ + subagents: { agents: { "general-purpose": { model: "claude-haiku-4.5" } } }, + }); const assistantMessage = await session.sendAndWait({ prompt: "What is your full name?" }); expect(assistantMessage?.data.content).toContain("GitHub"); @@ -251,6 +254,8 @@ describe("Sessions", () => { const systemMessage = getSystemMessage(traffic[0]); expect(systemMessage).toContain("GitHub"); expect(systemMessage).toContain(systemMessageSuffix); + expect(systemMessage?.match(/\n/g)).toHaveLength(1); + expect(systemMessage).toContain("- general-purpose: claude-haiku-4.5"); }); it("should create a session with replaced systemMessage config", async () => { @@ -259,6 +264,9 @@ describe("Sessions", () => { onPermissionRequest: approveAll, systemMessage: { mode: "replace", content: testSystemMessage }, }); + await session.rpc.tools.updateSubagentSettings({ + subagents: { agents: { "general-purpose": { model: "claude-haiku-4.5" } } }, + }); const assistantMessage = await session.sendAndWait({ prompt: "What is your full name?" }); expect(assistantMessage?.data.content).not.toContain("GitHub"); @@ -283,12 +291,17 @@ describe("Sessions", () => { sections: { tone: { action: "replace", content: customTone }, code_change_rules: { action: "remove" }, + last_instructions: { action: "remove" }, }, content: appendedContent, }, }); + await session.rpc.tools.updateSubagentSettings({ + subagents: { agents: { "general-purpose": { model: "claude-haiku-4.5" } } }, + }); - await session.send({ prompt: "Who are you?" }); + const assistantMessage = await session.sendAndWait({ prompt: "Who are you?" }); + expect(assistantMessage?.type).toBe("assistant.message"); // Validate the system message sent to the model const traffic = await waitForExchanges(); @@ -297,6 +310,15 @@ describe("Sessions", () => { expect(systemMessage).toContain(appendedContent); // The code_change_rules section should have been removed expect(systemMessage).not.toContain(""); + expect(systemMessage).not.toContain("\n"); + expect(systemMessage).not.toContain("- general-purpose: claude-haiku-4.5"); + expect(traffic[0].request.tools).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + function: expect.objectContaining({ name: "task" }), + }), + ]) + ); } ); @@ -400,26 +422,41 @@ describe("Sessions", () => { }); it("should resume a session using the same client", async () => { + let phase = "create session"; + const events: string[] = []; + const onEvent = (event: { type: string }) => { + events.push(event.type); + }; + onTestFailed(() => { + console.error("Same-client resume diagnostics", { phase, events }); + }); + // Create initial session - const session1 = await client.createSession({ onPermissionRequest: approveAll }); + const session1 = await client.createSession({ onPermissionRequest: approveAll, onEvent }); const sessionId = session1.sessionId; + phase = "first sendAndWait"; const answer = await session1.sendAndWait({ prompt: "What is 1+1?" }); expect(answer?.data.content).toContain("2"); // Resume using the same client + phase = "resume session"; await using session2 = await client.resumeSession(sessionId, { onPermissionRequest: approveAll, + onEvent, }); expect(session2.sessionId).toBe(sessionId); + phase = "read resumed history"; const messages = await session2.getEvents(); const assistantMessages = messages.filter((m) => m.type === "assistant.message"); expect(assistantMessages[assistantMessages.length - 1].data.content).toContain("2"); // Can continue the conversation statefully + phase = "second sendAndWait"; const secondAssistantMessage = await session2.sendAndWait({ prompt: "Now if you double that, what do you get?", }); expect(secondAssistantMessage?.data.content).toContain("4"); + phase = "disconnect resumed session"; }); it("should resume a session using a new client", async () => { @@ -657,11 +694,13 @@ describe("Sessions", () => { }); it("should create session with custom config dir", async () => { + // Stop the config directory's writer before the fixture removes its files. + await using customClient = createClient(); const customConfigDir = `${homeDir}/custom-config`; onTestFinished(async () => { await rm(customConfigDir, { recursive: true, force: true }).catch(() => {}); }); - await using session = await client.createSession({ + await using session = await customClient.createSession({ onPermissionRequest: approveAll, configDirectory: customConfigDir, }); diff --git a/nodejs/test/e2e/session_config.e2e.test.ts b/nodejs/test/e2e/session_config.e2e.test.ts index de81e8be1d..0abd7a56f5 100644 --- a/nodejs/test/e2e/session_config.e2e.test.ts +++ b/nodejs/test/e2e/session_config.e2e.test.ts @@ -1,4 +1,6 @@ import { describe, expect, it } from "vitest"; +import { execFileSync } from "node:child_process"; +import { realpathSync } from "node:fs"; import { writeFile, mkdir } from "fs/promises"; import { join } from "path"; import { @@ -10,6 +12,9 @@ import { } from "../../src/index.js"; import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js"; import { retry } from "./harness/sdkTestHelper.js"; +import { isByokBackend } from "./harness/testBackend"; + +const capiIt = it.skipIf(isByokBackend); describe("Session Configuration", async () => { const { copilotClient: client, workDir, openAiEndpoint, env } = await createSdkTestContext(); @@ -44,7 +49,7 @@ describe("Session Configuration", async () => { await session.disconnect(); }); - it("should create session with custom provider config", async () => { + capiIt("should create session with custom provider config", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, provider: { @@ -62,7 +67,8 @@ describe("Session Configuration", async () => { } }); - it("should accept blob attachments", async () => { + // Preserve the existing .NET BYOK exclusions: image replay diverges for provider-backed sessions. + capiIt("should accept blob attachments", async () => { // Write the image to disk so the model can view it if it tries const pngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; @@ -114,7 +120,7 @@ describe("Session Configuration", async () => { ); } - it("vision disabled then enabled via setModel", async () => { + capiIt("vision disabled then enabled via setModel", async () => { await writeFile(join(workDir, "test.png"), PNG_1X1); const session = await client.createSession({ @@ -145,7 +151,7 @@ describe("Session Configuration", async () => { await session.disconnect(); }); - it("vision enabled then disabled via setModel", async () => { + capiIt("vision enabled then disabled via setModel", async () => { await writeFile(join(workDir, "test.png"), PNG_1X1); const session = await client.createSession({ @@ -291,8 +297,26 @@ describe("Session Configuration", async () => { body: string; } + /** Visible Anthropic thinking, with the provider-reported count if any. */ + interface AnthropicReasoningFixture { + tokens?: number; + } + + const thinkingBlock = { + type: "thinking", + thinking: "Visible thinking is not measured usage.", + signature: "test-signature", + }; + + function reportedThinkingTokens(reasoning?: AnthropicReasoningFixture) { + return reasoning?.tokens === undefined + ? {} + : { output_tokens_details: { thinking_tokens: reasoning.tokens } }; + } + class RecordingRequestHandler extends CopilotRequestHandler { readonly records: InterceptedRequest[] = []; + reasoning?: AnthropicReasoningFixture; protected override async sendRequest( request: Request, @@ -301,7 +325,7 @@ describe("Session Configuration", async () => { const body = request.body ? await request.text() : ""; this.records.push({ url: request.url, body }); return isInferenceUrl(request.url) - ? buildInferenceResponse(request.url, body) + ? buildInferenceResponse(request.url, body, this.reasoning) : buildNonInferenceResponse(request.url); } @@ -334,7 +358,10 @@ describe("Session Configuration", async () => { }); } - function anthropicMessageStreamBody(text: string): string { + function anthropicMessageStreamBody( + text: string, + reasoning?: AnthropicReasoningFixture + ): string { const events: Array<[string, unknown]> = [ [ "message_start", @@ -356,25 +383,40 @@ describe("Session Configuration", async () => { "content_block_start", { type: "content_block_start", - index: 0, + index: reasoning ? 1 : 0, content_block: { type: "text", text: "" }, }, ], [ "content_block_delta", - { type: "content_block_delta", index: 0, delta: { type: "text_delta", text } }, + { + type: "content_block_delta", + index: reasoning ? 1 : 0, + delta: { type: "text_delta", text }, + }, ], - ["content_block_stop", { type: "content_block_stop", index: 0 }], + ["content_block_stop", { type: "content_block_stop", index: reasoning ? 1 : 0 }], [ "message_delta", { type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, - usage: { output_tokens: 7 }, + usage: { output_tokens: 7, ...reportedThinkingTokens(reasoning) }, }, ], ["message_stop", { type: "message_stop" }], ]; + if (reasoning) { + events.splice( + 1, + 0, + [ + "content_block_start", + { type: "content_block_start", index: 0, content_block: thinkingBlock }, + ], + ["content_block_stop", { type: "content_block_stop", index: 0 }] + ); + } return events .map(([event, data]) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`) .join(""); @@ -414,22 +456,29 @@ describe("Session Configuration", async () => { return json({}); } - function buildInferenceResponse(url: string, body: string): Response { + function buildInferenceResponse( + url: string, + body: string, + reasoning?: AnthropicReasoningFixture + ): Response { const u = url.toLowerCase(); const wantsStream = /"stream"\s*:\s*true/.test(body); if (u.endsWith("/messages")) { if (wantsStream) { - return sse(anthropicMessageStreamBody("OK from the synthetic stream.")); + return sse(anthropicMessageStreamBody("OK from the synthetic stream.", reasoning)); } return json({ id: "msg_stub_1", type: "message", role: "assistant", model: "claude-sonnet-5", - content: [{ type: "text", text: "OK from the synthetic stream." }], + content: [ + ...(reasoning ? [thinkingBlock] : []), + { type: "text", text: "OK from the synthetic stream." }, + ], stop_reason: "end_turn", stop_sequence: null, - usage: { input_tokens: 5, output_tokens: 7 }, + usage: { input_tokens: 5, output_tokens: 7, ...reportedThinkingTokens(reasoning) }, }); } return json({ @@ -551,7 +600,7 @@ describe("Session Configuration", async () => { await session1.disconnect(); }); - it("should enable citations for Anthropic file attachments on create", async () => { + capiIt("should enable citations for Anthropic file attachments on create", async () => { const handler = new RecordingRequestHandler(); const citationClient = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), @@ -584,7 +633,52 @@ describe("Session Configuration", async () => { } }); - it("should enable citations for Anthropic file attachments on resume", async () => { + it.each( + [false, true].flatMap((streaming) => + [undefined, 0, 7].map((tokens) => ({ streaming, tokens })) + ) + )( + "reports Anthropic reasoning tokens only when the provider does (streaming: $streaming, tokens: $tokens)", + async ({ streaming, tokens }) => { + const handler = new RecordingRequestHandler(); + handler.reasoning = { tokens }; + const usageClient = new CopilotClient({ + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + workingDirectory: workDir, + env, + gitHubToken: DEFAULT_GITHUB_TOKEN, + requestHandler: handler, + }); + await usageClient.start(); + try { + const session = await usageClient.createSession({ + onPermissionRequest: approveAll, + model: "claude-sonnet-5", + provider: createAnthropicProvider(), + streaming, + }); + try { + const usages: Array> = []; + session.on("assistant.usage", (event) => usages.push(event.data)); + const response = await session.sendAndWait({ prompt: "Reply briefly." }); + expect(response?.data.content).toBe("OK from the synthetic stream."); + expect(usages).toHaveLength(1); + expect(usages[0]).toMatchObject({ inputTokens: 5, outputTokens: 7 }); + if (tokens === undefined) { + expect(usages[0]).not.toHaveProperty("reasoningTokens"); + } else { + expect(usages[0].reasoningTokens).toBe(tokens); + } + } finally { + await session.disconnect(); + } + } finally { + await usageClient.stop(); + } + } + ); + + capiIt("should enable citations for Anthropic file attachments on resume", async () => { const handler = new RecordingRequestHandler(); const connectionToken = "ts-citation-resume-token"; const serverClient = new CopilotClient({ @@ -600,7 +694,9 @@ describe("Session Configuration", async () => { await serverClient.start(); try { - const session1 = await serverClient.createSession({ onPermissionRequest: approveAll }); + const session1 = await serverClient.createSession({ + onPermissionRequest: approveAll, + }); const port = (serverClient as unknown as { runtimePort: number | null }).runtimePort; expect(port).not.toBeNull(); const resumeClient = new CopilotClient({ @@ -693,7 +789,7 @@ describe("Session Configuration", async () => { await session1.disconnect(); }); - it("should refresh cached custom instructions only when creating with opt-in", async () => { + capiIt("should refresh cached custom instructions only when creating with opt-in", async () => { const projectDir = join(workDir, "instruction-refresh-project"); const instructionDir = join(workDir, "refresh-instructions"); const instructionFilesDir = join(instructionDir, ".github", "instructions"); @@ -755,7 +851,100 @@ describe("Session Configuration", async () => { } }); - it("should forward clientName in user-agent", async () => { + it("should include instructions discovered after repository creation in the next turn", async () => { + const projectDir = join(workDir, "deferred-repository-instructions"); + const marker = "SDK_RELOAD_DEFERRED_REPOSITORY_INSTRUCTIONS"; + await mkdir(projectDir); + + const handler = new RecordingRequestHandler(); + const refreshClient = new CopilotClient({ + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + workingDirectory: projectDir, + env, + gitHubToken: DEFAULT_GITHUB_TOKEN, + requestHandler: handler, + }); + const session = await refreshClient.createSession({ + onPermissionRequest: approveAll, + workingDirectory: projectDir, + model: "claude-sonnet-5", + provider: createAnthropicProvider(), + }); + + try { + const before = await session.rpc.metadata.snapshot(); + expect((await session.sendAndWait({ prompt: "Say OK." }))?.data.content).toBe( + "OK from the synthetic stream." + ); + await retry("observe the initial session context", async () => { + const metadata = await refreshClient.getSessionMetadata(session.sessionId); + expect(metadata?.context?.workingDirectory).toBe(projectDir); + }); + const initialContext = (await refreshClient.getSessionMetadata(session.sessionId)) + ?.context; + expect(initialContext?.gitRoot).toBeUndefined(); + expect(handler.inferenceRequests()).toHaveLength(1); + const firstRequest = JSON.parse(handler.inferenceRequests()[0].body) as { + system: unknown; + }; + expect(firstRequest.system).toBeDefined(); + expect(JSON.stringify(firstRequest.system)).not.toContain(marker); + + execFileSync("git", ["init", "--quiet"], { cwd: projectDir }); + const instructionsDir = join(projectDir, ".github"); + await mkdir(instructionsDir); + await writeFile( + join(instructionsDir, "copilot-instructions.md"), + `Repository instructions: ${marker}\n` + ); + + const reload = await session.rpc.customizations.reload(); + expect(reload.errors).toEqual([]); + expect(reload.outcomes).toEqual( + expect.arrayContaining([ + expect.objectContaining({ subsystem: "repositoryContext", status: "reloaded" }), + expect.objectContaining({ subsystem: "instructions", status: "reloaded" }), + ]) + ); + await retry("observe the refreshed repository context", async () => { + const gitRoot = (await refreshClient.getSessionMetadata(session.sessionId))?.context + ?.gitRoot; + expect(gitRoot && realpathSync.native(gitRoot)).toBe( + realpathSync.native(projectDir) + ); + }); + expect((await session.rpc.metadata.snapshot()).workingDirectory).toBe( + before.workingDirectory + ); + expect( + (await session.rpc.instructions.getSources()).sources.some( + (source) => + source.type === "repo" && + source.location === "repository" && + source.content.includes(marker) + ) + ).toBe(true); + + expect((await session.sendAndWait({ prompt: "Say OK again." }))?.data.content).toBe( + "OK from the synthetic stream." + ); + expect(handler.inferenceRequests()).toHaveLength(2); + const nextRequest = JSON.parse(handler.inferenceRequests()[1].body) as { + system: unknown; + }; + expect(nextRequest.system).toBeDefined(); + expect(JSON.stringify(nextRequest.system)).toContain(marker); + } finally { + try { + await session.disconnect(); + } finally { + expect(await refreshClient.stop()).toEqual([]); + } + } + }); + + // This asserts CAPI's user-agent; BYOK providers supply their own. + capiIt("should forward clientName in user-agent", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, clientName: CLIENT_NAME, @@ -772,7 +961,7 @@ describe("Session Configuration", async () => { await session.disconnect(); }); - it("should forward custom provider headers on create", async () => { + capiIt("should forward custom provider headers on create", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, model: "claude-sonnet-5", @@ -792,7 +981,7 @@ describe("Session Configuration", async () => { await session.disconnect(); }); - it("should forward custom provider headers on resume", async () => { + capiIt("should forward custom provider headers on resume", async () => { const session1 = await client.createSession({ onPermissionRequest: approveAll }); const sessionId = session1.sessionId; @@ -816,7 +1005,7 @@ describe("Session Configuration", async () => { await session2.disconnect(); }); - it("should forward provider wire model", async () => { + capiIt("should forward provider wire model", async () => { // Verifies that ProviderConfig.wireModel overrides the model name sent to // the provider API, while SessionConfig.model still drives runtime // configuration lookup (capabilities, prompts, reasoning behavior). @@ -845,7 +1034,7 @@ describe("Session Configuration", async () => { await session.disconnect(); }); - it("should use provider model id as wire model", async () => { + capiIt("should use provider model id as wire model", async () => { // ProviderConfig.modelId drives both the runtime resolved model AND the wire // model when wireModel is not specified. SessionConfig.model is intentionally // omitted so that modelId is the only model source. @@ -930,10 +1119,13 @@ describe("Session Configuration", async () => { } }); - it("should apply GitHub MCP tool config on create", async () => { + // Built-in GitHub MCP requires Copilot authentication, not BYOK API-key authentication. + capiIt("should apply GitHub MCP tool config on create", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, enableConfigDiscovery: true, + // Discover MCP configuration without starting unrelated standalone extensions. + requestExtensions: false, enableMcpApps: true, githubMcpToolConfig: { enableAllTools: true, diff --git a/nodejs/test/e2e/session_fs.e2e.test.ts b/nodejs/test/e2e/session_fs.e2e.test.ts index 9727de06e1..a987127965 100644 --- a/nodejs/test/e2e/session_fs.e2e.test.ts +++ b/nodejs/test/e2e/session_fs.e2e.test.ts @@ -4,7 +4,8 @@ import { SessionCompactionCompleteEvent } from "@github/copilot/sdk"; import { MemoryProvider, VirtualProvider } from "@platformatic/vfs"; -import { mkdtempSync, realpathSync } from "fs"; +import { createHash } from "crypto"; +import { existsSync, mkdtempSync, realpathSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { describe, expect, it, onTestFinished } from "vitest"; @@ -41,7 +42,12 @@ describe("Session Fs", async () => { const p = (sessionId: string, path: string) => `/${sessionId}${path.startsWith("/") ? path : "/" + path}`; - const { copilotClient: client, env } = await createSdkTestContext({ + const { + copilotClient: client, + createClient, + env, + openAiEndpoint, + } = await createSdkTestContext({ copilotClientOptions: { sessionFs: sessionFsConfig }, }); @@ -76,6 +82,74 @@ describe("Session Fs", async () => { } ); + it("should view an image that exists only in the binary session fs provider", async () => { + const imagePath = "/sdk-provider-image.png"; + const imageBytes = Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==", + "base64" + ); + const binaryClient = createClient({ + sessionFs: { ...sessionFsConfig, capabilities: { binary: true } }, + }); + try { + let requestedPath: string | undefined; + const session = await binaryClient.createSession({ + onPermissionRequest: approveAll, + modelCapabilities: { supports: { vision: true } }, + createSessionFsProvider: (session) => ({ + ...createSessionFsProvider(session), + async readFileBytes(path) { + requestedPath = path; + return provider.readFile(p(session.sessionId, path)); + }, + async writeFileBytes(path, content) { + await provider.writeFile(p(session.sessionId, path), content); + }, + }), + }); + await provider.mkdir(`/${session.sessionId}`, { recursive: true }); + await provider.writeFile(p(session.sessionId, imagePath), imageBytes); + expect(existsSync(imagePath)).toBe(false); + + const msg = await session.sendAndWait({ + prompt: "Use the view tool to view /sdk-provider-image.png, then reply with exactly SDK_PROVIDER_IMAGE_DONE.", + }); + expect(msg?.data.content).toContain("SDK_PROVIDER_IMAGE_DONE"); + expect(requestedPath).toBe(imagePath); + const events = await session.getEvents(); + const assetId = `sha256:${createHash("sha256").update(imageBytes).digest("hex")}`; + expect( + events.some( + (event) => + event.type === "session.binary_asset" && + event.data.assetId === assetId && + event.data.mimeType === "image/png" && + event.data.data === imageBytes.toString("base64") + ) + ).toBe(true); + expect( + events.some( + (event) => + event.type === "tool.execution_complete" && + event.data.success && + event.data.result?.binaryResultsForLlm?.some( + (result) => "assetId" in result && result.assetId === assetId + ) + ) + ).toBe(true); + expect( + (await openAiEndpoint.getRequests()) + .filter((request) => + ["/chat/completions", "/responses", "/v1/messages"].includes(request.url) + ) + .some((request) => request.body.includes(imageBytes.toString("base64"))) + ).toBe(true); + await session.disconnect(); + } finally { + await binaryClient.stop(); + } + }); + it("should load session data from fs provider on resume", async () => { const session1 = await client.createSession({ onPermissionRequest: approveAll, diff --git a/nodejs/test/e2e/set_tools.e2e.test.ts b/nodejs/test/e2e/set_tools.e2e.test.ts new file mode 100644 index 0000000000..70084129db --- /dev/null +++ b/nodejs/test/e2e/set_tools.e2e.test.ts @@ -0,0 +1,197 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +import { z } from "zod"; +import { approveAll, defineTool } from "../../src/index.js"; +import type { ParsedHttpExchange } from "../../../test/harness/replayingCapiProxy"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +// These scenarios, prompts, and tool results are shared with the other SDKs' +// set_tools E2E tests, which replay the same snapshots. +const FRUIT_PROMPT = "Use lookup_fruit to find the fruit for code 42."; +const FRUIT_AND_VEGETABLE_PROMPT = + "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 = "Use lookup_vegetable to find the vegetable for code 7."; + +function lookupFruit(fruit: string, calls: number[] = []) { + return defineTool("lookup_fruit", { + description: "Looks up the fruit for a numeric code", + parameters: z.object({ code: z.number().int().describe("Fruit code") }), + handler: ({ code }) => { + calls.push(code); + return fruit; + }, + }); +} + +function lookupVegetable(calls: number[] = []) { + return defineTool("lookup_vegetable", { + description: "Looks up the vegetable for a numeric code", + parameters: z.object({ code: z.number().int().describe("Vegetable code") }), + handler: ({ code }) => { + calls.push(code); + return "carrot"; + }, + }); +} + +function offeredTools(exchange: ParsedHttpExchange): string[] { + return (exchange.request.tools ?? []).map((tool) => + "function" in tool ? tool.function.name : "" + ); +} + +/** Whether the exchange's request carries `prompt` as a user message. */ +function includesPrompt(exchange: ParsedHttpExchange, prompt: string): boolean { + return exchange.request.messages.some( + (message) => + message.role === "user" && + (typeof message.content === "string" + ? message.content.includes(prompt) + : JSON.stringify(message.content).includes(prompt)) + ); +} + +describe("Live tool replacement", async () => { + const { copilotClient: client, openAiEndpoint } = await createSdkTestContext(); + + it("replaces tools on a created session", async () => { + const originalLookups: number[] = []; + const replacementLookups: number[] = []; + const vegetableLookups: number[] = []; + const session = await client.createSession({ + onPermissionRequest: approveAll, + tools: [ + lookupFruit("apple", originalLookups), + defineTool("retired_lookup", { + description: "Looks up a retired value", + handler: () => "retired", + }), + ], + }); + + const first = await session.sendAndWait({ prompt: FRUIT_PROMPT }); + expect(first?.data.content).toContain("apple"); + + await session.setTools([ + lookupFruit("dragonfruit", replacementLookups), + lookupVegetable(vegetableLookups), + ]); + + const second = await session.sendAndWait({ prompt: FRUIT_AND_VEGETABLE_PROMPT }); + expect(second?.data.content).toContain("dragonfruit"); + expect(second?.data.content).toContain("carrot"); + expect(originalLookups).toEqual([42]); + expect(replacementLookups).toEqual([42]); + expect(vegetableLookups).toEqual([7]); + + // Model requests after the replacement offer exactly the new tool set. + const exchanges = await openAiEndpoint.getExchanges(); + const replacedFrom = exchanges.findIndex((exchange) => + includesPrompt(exchange, FRUIT_AND_VEGETABLE_PROMPT) + ); + expect(replacedFrom).toBeGreaterThan(0); + for (const exchange of exchanges.slice(0, replacedFrom)) { + expect(offeredTools(exchange)).toEqual( + expect.arrayContaining(["lookup_fruit", "retired_lookup"]) + ); + expect(offeredTools(exchange)).not.toContain("lookup_vegetable"); + } + for (const exchange of exchanges.slice(replacedFrom)) { + expect(offeredTools(exchange)).toEqual( + expect.arrayContaining(["lookup_fruit", "lookup_vegetable"]) + ); + expect(offeredTools(exchange)).not.toContain("retired_lookup"); + } + + await session.disconnect(); + }); + + it("replaces tools on a resumed session", async () => { + const createdLookups: number[] = []; + const created = await client.createSession({ + onPermissionRequest: approveAll, + tools: [lookupFruit("apple", createdLookups)], + }); + const sessionId = created.sessionId; + const first = await created.sendAndWait({ prompt: FRUIT_PROMPT }); + expect(first?.data.content).toContain("apple"); + expect(createdLookups).toEqual([42]); + await created.disconnect(); + + const fruitLookups: number[] = []; + const vegetableLookups: number[] = []; + const resumed = await client.resumeSession(sessionId, { + onPermissionRequest: approveAll, + tools: [lookupFruit("apple", fruitLookups)], + }); + await resumed.setTools([lookupVegetable(vegetableLookups)]); + + const answer = await resumed.sendAndWait({ prompt: VEGETABLE_PROMPT }); + expect(answer?.data.content).toContain("carrot"); + expect(vegetableLookups).toEqual([7]); + expect(fruitLookups).toEqual([]); + + const exchanges = await openAiEndpoint.getExchanges(); + const replacedFrom = exchanges.findIndex((exchange) => + includesPrompt(exchange, VEGETABLE_PROMPT) + ); + expect(replacedFrom).toBeGreaterThan(0); + for (const exchange of exchanges.slice(replacedFrom)) { + expect(offeredTools(exchange)).toContain("lookup_vegetable"); + expect(offeredTools(exchange)).not.toContain("lookup_fruit"); + } + + await resumed.disconnect(); + }); + + it("keeps the previous tools when a replacement is rejected", async () => { + const originalLookups: number[] = []; + const replacementLookups: number[] = []; + const session = await client.createSession({ + onPermissionRequest: approveAll, + tools: [lookupFruit("apple", originalLookups)], + }); + + await expect( + session.setTools([ + lookupFruit("dragonfruit", replacementLookups), + defineTool("invalid.tool", { + description: "Has a name the runtime rejects", + handler: () => "never", + }), + ]) + ).rejects.toThrow(); + + const answer = await session.sendAndWait({ prompt: FRUIT_PROMPT }); + expect(answer?.data.content).toContain("apple"); + expect(originalLookups).toEqual([42]); + expect(replacementLookups).toEqual([]); + + await session.disconnect(); + }); + + it("removes all client tools with an empty set", async () => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + tools: [lookupFruit("apple")], + }); + + await session.setTools([]); + + const answer = await session.sendAndWait({ prompt: "Reply with exactly OK." }); + expect(answer?.data.content).toContain("OK"); + + // The client's tools are gone, while the runtime's built-in tools remain. + const exchanges = await openAiEndpoint.getExchanges(); + expect(exchanges.length).toBeGreaterThan(0); + for (const exchange of exchanges) { + expect(offeredTools(exchange)).not.toContain("lookup_fruit"); + expect(offeredTools(exchange)).toContain("view"); + } + + await session.disconnect(); + }); +}); diff --git a/nodejs/test/e2e/streaming_fidelity.e2e.test.ts b/nodejs/test/e2e/streaming_fidelity.e2e.test.ts index 98b8eb1884..89e1cb1dc1 100644 --- a/nodejs/test/e2e/streaming_fidelity.e2e.test.ts +++ b/nodejs/test/e2e/streaming_fidelity.e2e.test.ts @@ -5,9 +5,10 @@ import { describe, expect, it, onTestFinished } from "vitest"; import { SessionEvent, approveAll } from "../../src/index.js"; import { createSdkTestContext, isCI } from "./harness/sdkTestContext"; +import { isByokBackend, testBackend } from "./harness/testBackend"; describe("Streaming Fidelity", async () => { - const { copilotClient: client, createClient } = await createSdkTestContext(); + const { copilotClient: client, createClient, openAiEndpoint } = await createSdkTestContext(); it("should produce delta events when streaming is enabled", async () => { const session = await client.createSession({ @@ -77,7 +78,8 @@ describe("Streaming Fidelity", async () => { onPermissionRequest: approveAll, streaming: false, }); - await session.sendAndWait({ prompt: "What is 3 + 6?" }); + const firstAssistantMessage = await session.sendAndWait({ prompt: "What is 3 + 6?" }); + expect(firstAssistantMessage?.data.content).toContain("9"); await session.disconnect(); // Resume using a new client @@ -97,6 +99,26 @@ describe("Streaming Fidelity", async () => { }); expect(secondAssistantMessage?.data.content).toContain("18"); + const modelRequests = (await openAiEndpoint.getRequests()).filter((request) => + ["/chat/completions", "/v1/messages", "/responses"].includes(request.url) + ); + const endpoint = { + capi: "/chat/completions", + "anthropic-messages": "/v1/messages", + "openai-responses": "/responses", + "openai-completions": "/chat/completions", + }[testBackend]; + expect(modelRequests.length).toBeGreaterThanOrEqual(2); + for (const request of modelRequests) { + expect(request.url).toBe(endpoint); + if (isByokBackend) { + // Completions and CAPI share a URL; the BYOK credential distinguishes them. + expect(request.headers.authorization).toBe( + "Bearer fake-byok-credential-for-e2e-tests" + ); + } + } + // Should have streaming deltas before the final message const deltaEvents = events.filter((e) => e.type === "assistant.message_delta"); expect(deltaEvents.length).toBeGreaterThanOrEqual(1); diff --git a/nodejs/test/e2e/structured_output.e2e.test.ts b/nodejs/test/e2e/structured_output.e2e.test.ts index 0eb5957a61..e92125507f 100644 --- a/nodejs/test/e2e/structured_output.e2e.test.ts +++ b/nodejs/test/e2e/structured_output.e2e.test.ts @@ -14,6 +14,7 @@ import { } from "../../src/index.js"; import { createSdkTestContext, DEFAULT_GITHUB_TOKEN, isCI } from "./harness/sdkTestContext"; import { waitForCondition } from "./harness/sdkTestHelper"; +import { isByokBackend } from "./harness/testBackend"; describe("Structured output", async () => { const { copilotClient: client, openAiEndpoint } = await createSdkTestContext({ @@ -21,25 +22,30 @@ describe("Structured output", async () => { env: { COPILOT_CLI_ENABLED_FEATURE_FLAGS: "HYDRAFUSION,HYDRAFUSION_ROLLOUT" }, }, }); - const provider: ProviderConfig = { - type: "openai", - wireApi: "completions", - baseUrl: openAiEndpoint.url, - modelId: "gpt-4.1", - wireModel: "gpt-4.1", - apiKey: isCI ? DEFAULT_GITHUB_TOKEN : (process.env.GITHUB_TOKEN ?? DEFAULT_GITHUB_TOKEN), - headers: { - "Copilot-Integration-Id": "copilot-developer-cli", - "Copilot-Harness-Id": "copilot-sdk", - "X-GitHub-Api-Version": "2026-08-01", - }, - }; + const model = isByokBackend ? undefined : "gpt-4.1"; + const provider: ProviderConfig | undefined = isByokBackend + ? undefined + : { + type: "openai", + wireApi: "completions", + baseUrl: openAiEndpoint.url, + modelId: "gpt-4.1", + wireModel: "gpt-4.1", + apiKey: isCI + ? DEFAULT_GITHUB_TOKEN + : (process.env.GITHUB_TOKEN ?? DEFAULT_GITHUB_TOKEN), + headers: { + "Copilot-Integration-Id": "copilot-developer-cli", + "Copilot-Harness-Id": "copilot-sdk", + "X-GitHub-Api-Version": "2026-08-01", + }, + }; it("infers_typed_result_after_custom_tool", async () => { let calls = 0; const schema = z.object({ count: z.number().int(), color: z.string() }); const session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], @@ -71,7 +77,7 @@ describe("Structured output", async () => { expect(ordinary?.data.content.trim()).toBe("HELLO"); const exchanges = await openAiEndpoint.getExchanges(); expect(exchanges.length).toBeGreaterThanOrEqual(3); - for (const exchange of exchanges.slice(0, -1)) { + for (const exchange of isByokBackend ? [] : exchanges.slice(0, -1)) { expect(exchange.request).toHaveProperty( "response_format.json_schema.schema", schema.toJSONSchema() @@ -85,7 +91,7 @@ describe("Structured output", async () => { let calls = 0; let session: CopilotSession; session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], @@ -128,7 +134,7 @@ describe("Structured output", async () => { for (const exchange of exchanges.slice(1)) { expect(exchange.request).toHaveProperty("tool_choice", "none"); } - for (const exchange of exchanges) { + for (const exchange of isByokBackend ? [] : exchanges) { expect(exchange.request).toHaveProperty( "response_format.json_schema.schema", schema.toJSONSchema() @@ -141,7 +147,7 @@ describe("Structured output", async () => { let stops = 0; const replies: AssistantMessageEvent[] = []; const session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], @@ -188,7 +194,7 @@ describe("Structured output", async () => { const exchanges = await openAiEndpoint.getExchanges(); expect(exchanges).toHaveLength(3); expect(exchanges[1].request).toHaveProperty("tool_choice", "none"); - for (const exchange of exchanges) { + for (const exchange of isByokBackend ? [] : exchanges) { expect(exchange.request).toHaveProperty( "response_format.json_schema.schema", schema.toJSONSchema() @@ -247,7 +253,7 @@ describe("Structured output", async () => { releaseHook = resolve; }); const session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], @@ -319,7 +325,7 @@ describe("Structured output", async () => { const replies: AssistantMessageEvent[] = []; const schema = z.object({ answer: z.number().int() }); const session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], @@ -347,7 +353,7 @@ describe("Structured output", async () => { expect(replies[1].data.originatingMessageId).toBe(replies[0].data.originatingMessageId); const exchanges = await openAiEndpoint.getExchanges(); expect(exchanges).toHaveLength(2); - for (const exchange of exchanges) { + for (const exchange of isByokBackend ? [] : exchanges) { expect(exchange.request).toHaveProperty( "response_format.json_schema.schema", schema.toJSONSchema() @@ -362,7 +368,7 @@ describe("Structured output", async () => { const replies: AssistantMessageEvent[] = []; const schema = z.object({ answer: z.number().int() }); session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], @@ -394,7 +400,7 @@ describe("Structured output", async () => { expect(replies[1].data.originatingMessageId).toBe(replies[0].data.originatingMessageId); const exchanges = await openAiEndpoint.getExchanges(); expect(exchanges).toHaveLength(2); - for (const exchange of exchanges) { + for (const exchange of isByokBackend ? [] : exchanges) { expect(exchange.request).toHaveProperty( "response_format.json_schema.schema", schema.toJSONSchema() @@ -412,7 +418,7 @@ describe("Structured output", async () => { releaseTool = resolve; }); const session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], @@ -469,7 +475,7 @@ describe("Structured output", async () => { it("sends_explicit_schema_for_message_and_batch", async () => { const session = await client.createSession({ - model: "gpt-4.1", + model, provider, onPermissionRequest: approveAll, availableTools: [], diff --git a/nodejs/test/e2e/subagent_hooks.e2e.test.ts b/nodejs/test/e2e/subagent_hooks.e2e.test.ts index dbc3ca673b..a892d62c84 100644 --- a/nodejs/test/e2e/subagent_hooks.e2e.test.ts +++ b/nodejs/test/e2e/subagent_hooks.e2e.test.ts @@ -3,6 +3,7 @@ *--------------------------------------------------------------------------------------------*/ import { writeFile } from "fs/promises"; +import { randomUUID } from "node:crypto"; import { join } from "path"; import { describe, expect, it } from "vitest"; import type { @@ -11,10 +12,18 @@ import type { PreToolUseHookOutput, PostToolUseHookInput, PostToolUseHookOutput, + SubagentStartHookInput, + SubagentStopHookInput, } from "../../src/index.js"; import { approveAll, CopilotRequestHandler } from "../../src/index.js"; +import { responsesApiRequestToChatCompletion } from "../../../test/harness/responsesApiAdapter.js"; import { createSdkTestContext, isCI } from "./harness/sdkTestContext.js"; +const CHILD_CONTEXT = "Subagent start hook verified: read the requested file."; +const CHILD_CONTEXT_PREFIX = `${CHILD_CONTEXT}\n\n`; +const STOP_RESPONSE_PREFIX = "Subagent stop hook verified: "; +const SUBAGENT_TEST_TIMEOUT_MS = 120_000; + interface RequestRecord { url: string; agentId?: string; @@ -24,6 +33,8 @@ interface RequestRecord { class RecordingRequestHandler extends CopilotRequestHandler { readonly records: RequestRecord[] = []; + readonly childPromptsWithHookContext: string[] = []; + readonly parentRequestsWithModifiedResponse: string[] = []; protected override async sendRequest( request: Request, @@ -35,6 +46,48 @@ class RecordingRequestHandler extends CopilotRequestHandler { parentAgentId: ctx.parentAgentId, interactionType: ctx.interactionType, }); + if (isInferenceUrl(request.url)) { + const rawBody = await request.clone().text(); + const body = JSON.parse( + request.url.endsWith("/responses") + ? responsesApiRequestToChatCompletion(rawBody) + : rawBody + ) as { + messages?: { role: string; content?: unknown }[]; + input?: { role?: string; content?: unknown }[]; + }; + if (ctx.parentAgentId) { + // Responses uses input/input_text instead of messages/text. + for (const message of body.messages ?? body.input ?? []) { + if ( + message.role === "user" && + (typeof message.content === "string" || Array.isArray(message.content)) + ) { + const parts = + typeof message.content === "string" + ? [message.content] + : message.content + .filter( + ( + part + ): part is { + type: "text" | "input_text"; + text: string; + } => + (part?.type === "text" || + part?.type === "input_text") && + typeof part.text === "string" + ) + .map((part) => part.text); + this.childPromptsWithHookContext.push( + ...parts.filter((part) => part.includes(CHILD_CONTEXT_PREFIX)) + ); + } + } + } else if (JSON.stringify(body).includes(STOP_RESPONSE_PREFIX)) { + this.parentRequestsWithModifiedResponse.push(JSON.stringify(body)); + } + } return super.sendRequest(request, ctx); } } @@ -83,59 +136,154 @@ describe("Subagent hooks", async () => { }, }); - it("should invoke preToolUse and postToolUse hooks for sub-agent tool calls", async () => { - const hookLog: { kind: "pre" | "post"; toolName: string; sessionId: string }[] = []; - - const session = await client.createSession({ - onPermissionRequest: approveAll, - hooks: { - onPreToolUse: async (input: PreToolUseHookInput) => { - hookLog.push({ - kind: "pre", - toolName: input.toolName, - sessionId: input.sessionId, - }); - return { permissionDecision: "allow" } as PreToolUseHookOutput; - }, - onPostToolUse: async (input: PostToolUseHookInput) => { - hookLog.push({ - kind: "post", - toolName: input.toolName, - sessionId: input.sessionId, - }); - return null as PostToolUseHookOutput; + it( + "should apply subagent lifecycle hook outputs", + async () => { + const hookLog: { kind: "pre" | "post"; toolName: string; sessionId: string }[] = []; + const startInputs: { input: SubagentStartHookInput; invocationSessionId: string }[] = + []; + const stopInputs: { input: SubagentStopHookInput; invocationSessionId: string }[] = []; + const waitingText = + "I've launched an explore agent to read subagent-test.txt. Waiting for it to complete..."; + const finalText = + "The explore agent successfully read the file. The contents of **subagent-test.txt** are:\n\n```\nHello from subagent test!\n```"; + const parentSessionId = randomUUID(); + let releaseView!: () => void; + const parentWaiting = new Promise((resolve) => { + releaseView = resolve; + }); + + const session = await client.createSession({ + sessionId: parentSessionId, + onPermissionRequest: approveAll, + hooks: { + onPreToolUse: async (input: PreToolUseHookInput) => { + hookLog.push({ + kind: "pre", + toolName: input.toolName, + sessionId: input.sessionId, + }); + return { permissionDecision: "allow" } as PreToolUseHookOutput; + }, + onPostToolUse: async (input: PostToolUseHookInput) => { + hookLog.push({ + kind: "post", + toolName: input.toolName, + sessionId: input.sessionId, + }); + // A fast child can inject its result before the parent ever asks for the fixture's waiting reply. + if (input.toolName === "view" && input.sessionId !== parentSessionId) + await parentWaiting; + return null as PostToolUseHookOutput; + }, + onSubagentStart: (input, invocation) => { + startInputs.push({ input, invocationSessionId: invocation.sessionId }); + return { additionalContext: CHILD_CONTEXT }; + }, + onSubagentStop: (input, invocation) => { + stopInputs.push({ input, invocationSessionId: invocation.sessionId }); + return { modifiedResponse: `${STOP_RESPONSE_PREFIX}${input.response}` }; + }, }, - }, - }); + }); + // Create a file for the sub-agent to read + await writeFile(join(workDir, "subagent-test.txt"), "Hello from subagent test!"); - // Create a file for the sub-agent to read - await writeFile(join(workDir, "subagent-test.txt"), "Hello from subagent test!"); + const unsubscribe = session.on((event) => { + if ( + !event.agentId && + event.type === "assistant.message" && + event.data.content === waitingText + ) { + releaseView(); + } + }); + try { + const response = await session.sendAndWait({ + prompt: "Use the task tool to spawn an explore agent that reads the file subagent-test.txt in the current directory and reports its contents. You must use the task tool.", + }); + expect(response?.agentId ?? "").toBe(""); + expect(response?.data.content).toBe(finalText); + expect( + (await session.getEvents()) + .filter((event) => !event.agentId && event.type === "assistant.message") + .map((event) => event.data.content) + .filter((content) => content === waitingText || content === finalText) + ).toEqual([waitingText, finalText]); + } finally { + releaseView(); + unsubscribe(); + } - await session.sendAndWait({ - prompt: "Use the task tool to spawn an explore agent that reads the file subagent-test.txt in the current directory and reports its contents. You must use the task tool.", - }); + // Parent tool hooks fire for "task" + const taskPre = hookLog.find((h) => h.kind === "pre" && h.toolName === "task"); + expect( + taskPre, + "preToolUse should fire for the parent's 'task' tool call" + ).toBeDefined(); + + // Sub-agent tool hooks fire for "view" + const viewPre = hookLog.filter((h) => h.kind === "pre" && h.toolName === "view"); + const viewPost = hookLog.filter((h) => h.kind === "post" && h.toolName === "view"); + expect( + viewPre.length, + "preToolUse should fire for the sub-agent's 'view' tool call" + ).toBeGreaterThan(0); + expect( + viewPost.length, + "postToolUse should fire for the sub-agent's 'view' tool call" + ).toBeGreaterThan(0); - // Parent tool hooks fire for "task" - const taskPre = hookLog.find((h) => h.kind === "pre" && h.toolName === "task"); - expect(taskPre, "preToolUse should fire for the parent's 'task' tool call").toBeDefined(); - - // Sub-agent tool hooks fire for "view" - const viewPre = hookLog.filter((h) => h.kind === "pre" && h.toolName === "view"); - const viewPost = hookLog.filter((h) => h.kind === "post" && h.toolName === "view"); - expect( - viewPre.length, - "preToolUse should fire for the sub-agent's 'view' tool call" - ).toBeGreaterThan(0); - expect( - viewPost.length, - "postToolUse should fire for the sub-agent's 'view' tool call" - ).toBeGreaterThan(0); - - // input.sessionId distinguishes parent from sub-agent: parent tools and - // sub-agent tools carry different sessionIds - expect(viewPre[0].sessionId).not.toBe(taskPre!.sessionId); - expectSubagentRequestMetadata(requestHandler.records); - - await session.disconnect(); - }, 120_000); + // input.sessionId distinguishes parent from sub-agent: parent tools and + // sub-agent tools carry different sessionIds + expect(viewPre[0].sessionId).not.toBe(taskPre!.sessionId); + expectSubagentRequestMetadata(requestHandler.records); + + await expect + .poll(() => stopInputs.length, { timeout: SUBAGENT_TEST_TIMEOUT_MS }) + .toBeGreaterThan(0); + + const start = startInputs.find(({ input }) => input.agentName === "explore"); + const stop = stopInputs.find(({ input }) => input.agentType === "explore"); + expect(start, "subagentStart should fire for the explore agent").toBeDefined(); + expect(stop, "subagentStop should fire for the explore agent").toBeDefined(); + expect(start!.invocationSessionId).toBe(session.sessionId); + expect(start!.input.sessionId).toBe(session.sessionId); + expect(start!.input.timestamp).toBeInstanceOf(Date); + expect(start!.input.workingDirectory).toBe(workDir); + expect(typeof start!.input.transcriptPath).toBe("string"); + expect(start!.input.agentDisplayName).toBeUndefined(); + expect(start!.input.agentDescription).toBeUndefined(); + + expect(stop!.invocationSessionId).toBe(session.sessionId); + expect(stop!.input.sessionId).toBe(session.sessionId); + expect(stop!.input.timestamp).toBeInstanceOf(Date); + expect(stop!.input.timestamp.getTime()).toBeGreaterThanOrEqual( + start!.input.timestamp.getTime() + ); + expect(stop!.input.workingDirectory).toBe(workDir); + expect(stop!.input.transcriptPath).toBe(start!.input.transcriptPath); + expect(stop!.input.agentName).toBe(start!.input.agentName); + expect(stop!.input.agentDisplayName).toBe(start!.input.agentDisplayName); + expect(stop!.input.agentDescription).toBe(start!.input.agentDescription); + expect(stop!.input.agentId).toBeTruthy(); + expect(stop!.input.agentType).toBe("explore"); + expect(stop!.input.stopReason).toBe("end_turn"); + expect(stop!.input.response).toContain("Hello from subagent test!"); + + expect( + requestHandler.childPromptsWithHookContext.some((prompt) => + prompt.includes(`${CHILD_CONTEXT_PREFIX}Read the file "subagent-test.txt"`) + ), + "the start hook's additionalContext should be prepended to the child's model prompt" + ).toBe(true); + await expect + .poll(() => requestHandler.parentRequestsWithModifiedResponse.length, { + timeout: SUBAGENT_TEST_TIMEOUT_MS, + }) + .toBeGreaterThan(0); + await session.disconnect(); + }, + SUBAGENT_TEST_TIMEOUT_MS + ); }); diff --git a/nodejs/test/e2e/subagent_prompt_resume.e2e.test.ts b/nodejs/test/e2e/subagent_prompt_resume.e2e.test.ts new file mode 100644 index 0000000000..2e82470012 --- /dev/null +++ b/nodejs/test/e2e/subagent_prompt_resume.e2e.test.ts @@ -0,0 +1,401 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { writeFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import type { + CopilotRequestContext, + CopilotSession, + CustomAgentConfig, + MCPStdioServerConfig, +} from "../../src/index.js"; +import { approveAll, CopilotRequestHandler } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const TEST_MCP_SERVER = resolve(__dirname, "../../../test/harness/test-mcp-server.mjs"); +const TEST_HARNESS_DIR = dirname(TEST_MCP_SERVER); + +interface InferenceRequest { + agentId?: string; + parentAgentId?: string; + systemPrompt: string; + toolNames: string[]; +} + +class RecordingRequestHandler extends CopilotRequestHandler { + readonly inferenceRequests: InferenceRequest[] = []; + + protected override async sendRequest( + request: Request, + ctx: CopilotRequestContext + ): Promise { + const body = request.body ? await request.text() : ""; + if (isInferenceUrl(request.url)) { + this.inferenceRequests.push({ + agentId: ctx.agentId, + parentAgentId: ctx.parentAgentId, + systemPrompt: systemPromptFromRequest(request.url, body), + toolNames: toolNamesFromRequest(body), + }); + return inferenceResponse(request.url); + } + return bootstrapResponse(request.url); + } +} + +function toolNamesFromRequest(body: string): string[] { + const request = JSON.parse(body) as { + tools?: Array<{ name?: string; function?: { name?: string } }>; + }; + return (request.tools ?? []) + .map((tool) => tool.name ?? tool.function?.name) + .filter((name): name is string => name !== undefined); +} + +function isInferenceUrl(url: string): boolean { + const normalized = url.toLowerCase(); + return ( + normalized.endsWith("/chat/completions") || + normalized.endsWith("/responses") || + normalized.endsWith("/v1/messages") || + normalized.endsWith("/messages") + ); +} + +function systemPromptFromRequest(url: string, body: string): string { + const request = JSON.parse(body) as { + instructions?: string; + messages?: Array<{ role?: string; content?: string }>; + system?: string | Array<{ text?: string }>; + }; + const normalized = url.toLowerCase(); + if (normalized.endsWith("/responses")) { + return request.instructions ?? ""; + } + if (normalized.endsWith("/messages")) { + return typeof request.system === "string" + ? request.system + : (request.system ?? []).map((block) => block.text ?? "").join(""); + } + return request.messages?.find((message) => message.role === "system")?.content ?? ""; +} + +function json(body: unknown): Response { + return new Response(JSON.stringify(body), { + status: 200, + headers: { "content-type": "application/json" }, + }); +} + +function bootstrapResponse(url: string): Response { + const normalized = url.toLowerCase(); + if (normalized.endsWith("/models")) { + return json({ + data: [ + { + id: "claude-sonnet-5", + name: "Claude Sonnet 5", + object: "model", + vendor: "Anthropic", + version: "1", + preview: false, + model_picker_enabled: true, + capabilities: { + type: "chat", + family: "claude-sonnet-5", + tokenizer: "o200k_base", + limits: { + max_context_window_tokens: 200000, + max_output_tokens: 8192, + }, + supports: { + streaming: true, + tool_calls: true, + parallel_tool_calls: true, + vision: true, + }, + }, + }, + ], + }); + } + if (normalized.includes("/models/session")) { + return json({}); + } + if (normalized.includes("/policy")) { + return json({ state: "enabled" }); + } + return json({}); +} + +function inferenceResponse(url: string): Response { + if (url.toLowerCase().endsWith("/messages")) { + const events: Array<[string, unknown]> = [ + [ + "message_start", + { + type: "message_start", + message: { + id: "msg_subagent_prompt_resume", + type: "message", + role: "assistant", + model: "claude-sonnet-5", + content: [], + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 5, output_tokens: 1 }, + }, + }, + ], + [ + "content_block_start", + { + type: "content_block_start", + index: 0, + content_block: { type: "text", text: "" }, + }, + ], + [ + "content_block_delta", + { + type: "content_block_delta", + index: 0, + delta: { type: "text_delta", text: "SUBAGENT_TURN_COMPLETE" }, + }, + ], + ["content_block_stop", { type: "content_block_stop", index: 0 }], + [ + "message_delta", + { + type: "message_delta", + delta: { stop_reason: "end_turn", stop_sequence: null }, + usage: { output_tokens: 4 }, + }, + ], + ["message_stop", { type: "message_stop" }], + ]; + return new Response( + events + .map(([event, data]) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`) + .join(""), + { + status: 200, + headers: { "content-type": "text/event-stream" }, + } + ); + } + return json({ + id: "chatcmpl-subagent-prompt-resume", + object: "chat.completion", + created: 1, + model: "claude-sonnet-5", + choices: [ + { + index: 0, + message: { role: "assistant", content: "SUBAGENT_TURN_COMPLETE" }, + finish_reason: "stop", + }, + ], + usage: { prompt_tokens: 5, completion_tokens: 4, total_tokens: 9 }, + }); +} + +describe("Subagent prompt resume", async () => { + const requestHandler = new RecordingRequestHandler(); + const { copilotClient: client, env } = await createSdkTestContext({ + copilotClientOptions: { + requestHandler, + env: { COPILOT_EXP_COPILOT_CLI_SESSION_BASED_SUBAGENTS: "true" }, + }, + }); + + async function waitForIdleAgent(session: CopilotSession, agentId: string): Promise { + await waitForCondition( + async () => { + const task = (await session.rpc.tasks.list()).tasks.find( + (candidate) => candidate.id === agentId + ); + return task?.status === "idle"; + }, + { timeoutMessage: `Agent ${agentId} did not become idle.` } + ); + } + + async function runResumeScenario( + trigger: (session: CopilotSession) => Promise, + createOptions: Parameters[0] = {}, + beforeAgentStart: (session: CopilotSession) => Promise = async () => {}, + agentType = "explore" + ): Promise<[InferenceRequest, InferenceRequest]> { + await writeFile(join(env.COPILOT_HOME, "settings.json"), "{}"); + const requestBaseline = requestHandler.inferenceRequests.length; + const session = await client.createSession({ + model: "claude-sonnet-5", + onPermissionRequest: approveAll, + ...createOptions, + }); + + try { + await beforeAgentStart(session); + const { agentId } = await session.rpc.tasks.startAgent({ + agentType, + prompt: "Reply with exactly SUBAGENT_TURN_COMPLETE.", + name: "prompt-resume", + description: "Exercise an idle subagent follow-up.", + model: "claude-sonnet-5", + }); + await waitForIdleAgent(session, agentId); + + await trigger(session); + const sent = await session.rpc.tasks.sendMessage({ + id: agentId, + message: "Reply again with exactly SUBAGENT_TURN_COMPLETE.", + }); + expect(sent.sent).toBe(true); + await waitForCondition( + async () => { + const childRequests = requestHandler.inferenceRequests + .slice(requestBaseline) + .filter((request) => request.agentId === agentId && request.parentAgentId); + const task = (await session.rpc.tasks.list()).tasks.find( + (candidate) => candidate.id === agentId + ); + return childRequests.length === 2 && task?.status === "idle"; + }, + { timeoutMessage: "Agent did not complete its follow-up turn." } + ); + + const childRequests = requestHandler.inferenceRequests + .slice(requestBaseline) + .filter((request) => request.agentId === agentId && request.parentAgentId); + expect(childRequests).toHaveLength(2); + return [childRequests[0]!, childRequests[1]!]; + } finally { + const tasks = await session.rpc.tasks.list(); + for (const task of tasks.tasks) { + if ( + task.type === "agent" && + (task.status === "running" || task.status === "idle") + ) { + await session.rpc.tasks.cancel({ id: task.id }); + } + } + await session.disconnect(); + } + } + + function expectAdaptedExplorePrompt(prompt: string): void { + expect(prompt).toMatch(/^You are an exploration agent\./); + expect(prompt).not.toContain("You are GitHub Copilot, an AI coding agent built by GitHub."); + expect(prompt).not.toContain(""); + } + + function expectAdaptedGeneralPurposePrompt(prompt: string): void { + expect(prompt).toMatch( + /^As a sub-agent, complete your parent's task yourself; use another general-purpose agent only if the parent explicitly requests nested delegation\./ + ); + expect(prompt).toContain("You are running in non-interactive mode"); + } + + async function changePersistedSettings(): Promise { + await writeFile( + join(env.COPILOT_HOME, "settings.json"), + JSON.stringify({ subagents: { maxDepth: 4 } }) + ); + } + + it("preserves the adapted prompt on an ordinary idle-agent wake", async () => { + const [initial, resumed] = await runResumeScenario(async () => {}); + + expectAdaptedExplorePrompt(initial.systemPrompt); + expect(resumed.systemPrompt).toBe(initial.systemPrompt); + }); + + it("preserves the adapted prompt after persisted settings rebuild the child context", async () => { + const [initial, resumed] = await runResumeScenario(changePersistedSettings); + + expectAdaptedExplorePrompt(initial.systemPrompt); + expect(resumed.systemPrompt).toBe(initial.systemPrompt); + }); + + it("preserves the general-purpose prompt after persisted settings rebuild the child context", async () => { + const [initial, resumed] = await runResumeScenario( + changePersistedSettings, + {}, + async () => {}, + "general-purpose" + ); + + expectAdaptedGeneralPurposePrompt(initial.systemPrompt); + expect(resumed.systemPrompt).toBe(initial.systemPrompt); + }); + + it("preserves a user-defined custom-agent prompt after persisted settings rebuild the child context", async () => { + const customAgent: CustomAgentConfig = { + name: "prompt-resume-custom", + displayName: "Prompt Resume Custom", + description: "Verifies user-defined subagent prompt resumption.", + prompt: "You are the user-defined prompt resume agent.", + tools: [], + }; + const [initial, resumed] = await runResumeScenario( + changePersistedSettings, + { customAgents: [customAgent] }, + async () => {}, + customAgent.name + ); + + expect(initial.systemPrompt).toContain(customAgent.prompt); + expect(initial.systemPrompt).not.toContain( + "You are GitHub Copilot, an AI coding agent built by GitHub." + ); + expect(resumed.systemPrompt).toBe(initial.systemPrompt); + }); + + it("rebuilds the adapted prompt after the inherited MCP catalog changes", async () => { + const serverName = "prompt-resume-mcp"; + const config: MCPStdioServerConfig = { + type: "local", + command: process.execPath, + args: [TEST_MCP_SERVER, "--server-name", serverName], + workingDirectory: TEST_HARNESS_DIR, + tools: ["*"], + }; + const [initial, resumed] = await runResumeScenario( + async (session) => { + await session.rpc.mcp.stopServer({ serverName }); + await waitForCondition( + async () => !(await session.rpc.mcp.isServerRunning({ serverName })).running, + { timeoutMessage: `MCP server ${serverName} did not stop.` } + ); + }, + { + mcpServers: { [serverName]: config }, + }, + async (session) => { + await waitForCondition( + async () => + (await session.rpc.mcp.list()).servers.some( + (server) => server.name === serverName && server.status === "connected" + ), + { timeoutMessage: `MCP server ${serverName} did not connect.` } + ); + await session.sendAndWait({ + prompt: "Reply with exactly ROOT_TOOL_CONTEXT_READY.", + }); + }, + "general-purpose" + ); + + expectAdaptedGeneralPurposePrompt(initial.systemPrompt); + expectAdaptedGeneralPurposePrompt(resumed.systemPrompt); + expect(initial.toolNames.some((name) => name.includes("get_env"))).toBe(true); + expect(resumed.toolNames.some((name) => name.includes("get_env"))).toBe(false); + }); +}); diff --git a/nodejs/test/e2e/suspend.e2e.test.ts b/nodejs/test/e2e/suspend.e2e.test.ts index 2c8639ad38..64595b2d0c 100644 --- a/nodejs/test/e2e/suspend.e2e.test.ts +++ b/nodejs/test/e2e/suspend.e2e.test.ts @@ -58,11 +58,11 @@ function onTestFinishedStop(client: CopilotClient): void { } describe("Suspend RPC", async () => { - const { copilotClient: client, env, workDir } = await createSdkTestContext(); + const { copilotClient: client, env, workDir, createClient } = await createSdkTestContext(); const SHARED_TOKEN = "suspend-shared-test-token"; function createTcpServer(): CopilotClient { - const server = new CopilotClient({ + const server = createClient({ workingDirectory: workDir, env, gitHubToken: DEFAULT_GITHUB_TOKEN, @@ -76,7 +76,7 @@ describe("Suspend RPC", async () => { } function createConnectingClient(cliUrl: string): CopilotClient { - const connectedClient = new CopilotClient({ + const connectedClient = createClient({ connection: RuntimeConnection.forUri(cliUrl, { connectionToken: SHARED_TOKEN }), }); onTestFinishedStop(connectedClient); diff --git a/nodejs/test/e2e/system_message_sections.e2e.test.ts b/nodejs/test/e2e/system_message_sections.e2e.test.ts index 51380cf4ba..f984d8a9b6 100644 --- a/nodejs/test/e2e/system_message_sections.e2e.test.ts +++ b/nodejs/test/e2e/system_message_sections.e2e.test.ts @@ -2,12 +2,20 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { describe, expect, it } from "vitest"; -import { approveAll } from "../../src/index.js"; +import { createServer } from "node:http"; +import { text } from "node:stream/consumers"; +import { describe, expect, it, onTestFinished } from "vitest"; +import { + approveAll, + type CopilotSession, + type NamedProviderConfig, + type ProviderModelConfig, +} from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; describe("System message sections", async () => { - const { copilotClient: client } = await createSdkTestContext(); + const { copilotClient: client, createClient, workDir } = await createSdkTestContext(); it("should_use_replaced_identity_section_in_response", async () => { const session = await client.createSession({ @@ -62,4 +70,160 @@ describe("System message sections", async () => { await session.disconnect(); }); + + it( + "should_move_autopilot_instructions_out_of_a_non_preamble_customized_system_prompt", + { timeout: 90_000 }, + async () => { + let modelRequest: + | { + messages?: Array<{ role?: string; content?: string }>; + } + | undefined; + let modelFailure: Error | undefined; + const modelServer = createServer((request, response) => { + void (async () => { + modelRequest ??= JSON.parse(await text(request)); + response.writeHead(200, { "content-type": "application/json" }); + response.end( + JSON.stringify({ + id: "mode-customization-completion", + object: "chat.completion", + created: 0, + model: "test-model", + choices: [ + { + index: 0, + message: { + role: "assistant", + content: null, + tool_calls: [ + { + id: "mode-customization-task-complete", + type: "function", + function: { + name: "task_complete", + arguments: + '{"summary":"MODE_CUSTOMIZATION_REPRO_OK"}', + }, + }, + ], + }, + finish_reason: "tool_calls", + }, + ], + usage: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20 }, + }) + ); + })().catch((error: unknown) => { + modelFailure = error instanceof Error ? error : new Error(String(error)); + response.writeHead(500).end(); + }); + }); + const localClient = createClient(); + let session: CopilotSession | undefined; + let serverStart: Promise | undefined; + onTestFinished(async () => { + const errors: unknown[] = []; + if (session) { + try { + await session.disconnect(); + } catch (error) { + errors.push(error); + } + } + try { + errors.push(...(await localClient.stop())); + } catch (error) { + errors.push(error); + } + if (serverStart) { + const [startup] = await Promise.allSettled([serverStart]); + if (startup.status === "rejected") { + errors.push(startup.reason); + } + } + modelServer.closeAllConnections(); + if (modelServer.listening) { + try { + await new Promise((resolve, reject) => { + modelServer.close((error) => (error ? reject(error) : resolve())); + }); + } catch (error) { + errors.push(error); + } + } + if (errors.length) { + throw new AggregateError(errors, "Mode customization fixture cleanup failed"); + } + }); + serverStart = new Promise((resolve, reject) => { + modelServer.once("error", reject); + modelServer.listen(0, "127.0.0.1", resolve); + }); + await serverStart; + const address = modelServer.address(); + if (!address || typeof address === "string") { + throw new Error("Missing local model server address"); + } + + const providers: NamedProviderConfig[] = [ + { + name: "local", + type: "openai", + baseUrl: `http://127.0.0.1:${address.port}`, + apiKey: "test", + wireApi: "completions", + }, + ]; + const models: ProviderModelConfig[] = [ + { id: "model", provider: "local", modelId: "test-model", wireModel: "test-model" }, + ]; + session = await localClient.createSession({ + onPermissionRequest: approveAll, + workingDirectory: workDir, + providers, + models, + model: "local/model", + systemMessage: { + mode: "customize", + sections: { + identity: { + action: "replace", + content: "You are a customized coding assistant.", + }, + }, + }, + }); + + await session.rpc.mode.set({ mode: "autopilot" }); + await session.send({ + prompt: "Call task_complete with summary MODE_CUSTOMIZATION_REPRO_OK.", + }); + await waitForCondition(() => modelRequest !== undefined, { + timeoutMs: 30_000, + timeoutMessage: "The local model did not receive the autopilot request", + }); + await session.abort(); + + if (modelFailure) { + throw modelFailure; + } + expect(modelRequest).toBeDefined(); + const systemMessage = getMessage(modelRequest!, "system"); + const userMessage = getMessage(modelRequest!, "user"); + expect(systemMessage).toContain("You are a customized coding assistant."); + expect(systemMessage).not.toContain(""); + expect(userMessage).toContain(""); + expect(userMessage).toContain(""); + } + ); }); + +function getMessage( + request: { messages?: Array<{ role?: string; content?: string }> }, + role: "system" | "user" +): string | undefined { + const message = request.messages?.find((candidate) => candidate.role === role); + return message?.content; +} diff --git a/nodejs/test/e2e/system_message_transform.e2e.test.ts b/nodejs/test/e2e/system_message_transform.e2e.test.ts index ef37c39e9a..55af45fef9 100644 --- a/nodejs/test/e2e/system_message_transform.e2e.test.ts +++ b/nodejs/test/e2e/system_message_transform.e2e.test.ts @@ -54,7 +54,8 @@ describe("System message transform", async () => { }); it("should apply transform modifications to section content", async () => { - const session = await client.createSession({ + let lastInstructions = ""; + await using session = await client.createSession({ onPermissionRequest: approveAll, systemMessage: { mode: "customize", @@ -64,22 +65,37 @@ describe("System message transform", async () => { return content + "\nTRANSFORM_MARKER"; }, }, + last_instructions: { + action: (content) => { + lastInstructions = content; + return content.replace( + /\n*\n[\s\S]*?<\/subagent_model_preferences>/g, + "" + ); + }, + }, }, }, }); + await session.rpc.tools.updateSubagentSettings({ + subagents: { agents: { "general-purpose": { model: "claude-haiku-4.5" } } }, + }); await writeFile(join(workDir, "hello.txt"), "Hello!"); - await session.sendAndWait({ + const assistantMessage = await session.sendAndWait({ prompt: "Read the contents of hello.txt", }); + expect(assistantMessage?.type).toBe("assistant.message"); // Verify the transform result was actually applied to the system message const traffic = await openAiEndpoint.getExchanges(); const systemMessage = getSystemMessage(traffic[0]); expect(systemMessage).toContain("TRANSFORM_MARKER"); - - await session.disconnect(); + expect(lastInstructions.match(/\n/g)).toHaveLength(1); + expect(lastInstructions).toContain("- general-purpose: claude-haiku-4.5"); + expect(systemMessage).not.toContain("\n"); + expect(systemMessage).not.toContain("- general-purpose: claude-haiku-4.5"); }); it("should work with static overrides and transforms together", async () => { diff --git a/nodejs/test/e2e/telemetry-skills.e2e.test.ts b/nodejs/test/e2e/telemetry-skills.e2e.test.ts new file mode 100644 index 0000000000..92dbc80f64 --- /dev/null +++ b/nodejs/test/e2e/telemetry-skills.e2e.test.ts @@ -0,0 +1,470 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { mkdir, readFile, rename, writeFile } from "fs/promises"; +import { join } from "path"; +import { fileURLToPath } from "url"; +import { describe, expect, it } from "vitest"; +import { approveAll, RuntimeConnection } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +interface Span { + type: string; + name: string; + traceId: string; + spanId: string; + parentSpanId: string; + kind: number; + attributes: Record; + status: { code: number }; + events: { name: string; attributes: Record }[]; +} + +async function spansFrom(path: string): Promise { + const lines = (await readFile(path, "utf8")).split("\n").filter((line) => line.trim()); + return lines.map((line): Span => JSON.parse(line)).filter((entry) => entry.type === "span"); +} + +async function createSkill( + workDir: string, + directory = "skills", + name = "review", + description = "OTel skill description sentinel" +): Promise { + const root = join(workDir, directory, name); + await mkdir(join(root, "references"), { recursive: true }); + await mkdir(join(root, "scripts"), { recursive: true }); + await writeFile( + join(root, "SKILL.md"), + `---\nname: ${name}\ndescription: ${description}\n---\nFollow the user's explicit instructions. The verification word is OTEL_SKILL_ORCHID.\n` + ); + await writeFile(join(root, "references", "policy.txt"), "OTEL_RESOURCE_SAPPHIRE"); + await writeFile( + join(root, "scripts", "check.cjs"), + 'console.log("OTEL_SCRIPT_AMBER"); process.exit(0);\n' + ); + return root; +} + +function expectNoLegacySkillFields(attributes: Record): void { + for (const name of [ + "github.copilot.tool.parameters.skill_name", + "github.copilot.skill.name", + "github.copilot.skill.path", + ]) { + expect(attributes).not.toHaveProperty(name); + } +} + +const shell = process.platform === "win32" ? "powershell" : "bash"; + +function expectShellLauncher(span: Span): void { + expect(span.attributes["process.executable.name"]).toMatch( + process.platform === "win32" ? /^(pwsh|powershell)(\.exe)?$/i : /^bash$/ + ); +} + +for (const captureContent of [false, true]) { + describe(`Skill telemetry capture ${captureContent}`, async () => { + const { createClient, workDir } = await createSdkTestContext({ + copilotClientOptions: { + connection: RuntimeConnection.forStdio(), + }, + }); + + it( + `exports loader resource and script facts capture ${captureContent}`, + { timeout: 90_000 }, + async () => { + const telemetryFile = "loader-telemetry.jsonl"; + await using client = createClient({ + telemetry: { exporterType: "file", filePath: telemetryFile, captureContent }, + }); + const root = await createSkill(workDir); + await writeFile(join(workDir, "policy.txt"), "OTEL_UNRELATED_POLICY"); + const session = await client.createSession({ + enableConfigDiscovery: false, + skillDirectories: [join(workDir, "skills")], + onPermissionRequest: approveAll, + }); + const calls: { id: string; tool: string }[] = []; + const exits = new Map(); + session.on("tool.execution_start", (event) => { + calls.push({ id: event.data.toolCallId, tool: event.data.toolName }); + }); + session.on("tool.execution_complete", (event) => { + const exit = event.data.shellExecution?.exitCode; + if (exit !== undefined) { + exits.set(event.data.toolCallId, exit); + } + }); + const response = await session.sendAndWait( + { + prompt: + "Use the skill tool to load review. Then use view to read skills/review/references/policy.txt. " + + "Then use the shell tool in sync mode to run exactly: node './skills/review/scripts/check.cjs'. " + + "Then use view to read policy.txt in the working directory, outside the skill. " + + "Then use view to read skills/review/SKILL.md, and use view to list the skills/review directory. " + + "After all six calls finish, reply with exactly OTEL_SKILL_FLOW_DONE.", + }, + 90_000 + ); + expect(response?.data.content).toContain("OTEL_SKILL_FLOW_DONE"); + await session.disconnect(); + expect(await client.stop()).toEqual([]); + + const spans = await spansFrom(join(workDir, telemetryFile)); + const tools = spans.filter( + (span) => span.attributes["gen_ai.operation.name"] === "execute_tool" + ); + expect(tools).toHaveLength(6); + expect(tools.every((span) => span.kind === 0)).toBe(true); + expect(calls.filter((call) => call.tool === "skill")).toHaveLength(1); + for (const call of calls) { + expect( + tools.filter((span) => span.attributes["gen_ai.tool.call.id"] === call.id) + ).toHaveLength(1); + } + const loader = tools.find( + (span) => span.attributes["gen_ai.tool.name"] === "skill" + )!; + expect(loader.name).toBe("execute_tool skill review"); + expect(loader.attributes["gen_ai.skill.name"]).toBe("review"); + const reader = tools.find( + (span) => + span.attributes["gen_ai.skill.resource.name"] === "references/policy.txt" + )!; + expect(reader.name).toBe("execute_tool view review references/policy.txt"); + const command = tools.find( + (span) => span.attributes["gen_ai.skill.resource.name"] === "scripts/check.cjs" + )!; + expect(command.name).toBe(`execute_tool ${shell} review scripts/check.cjs`); + expect(command.attributes["process.exit.code"]).toBe(0); + expect(command.attributes["process.exit.code"]).toBe( + exits.get(String(command.attributes["gen_ai.tool.call.id"])) + ); + expectShellLauncher(command); + expect(reader.parentSpanId).toBe(loader.parentSpanId); + expect(command.parentSpanId).toBe(loader.parentSpanId); + expect(reader.traceId).toBe(loader.traceId); + expect(command.traceId).toBe(loader.traceId); + const unrelated = tools.filter( + (span) => span.attributes["gen_ai.tool.name"] === "view" && span !== reader + ); + expect(unrelated).toHaveLength(3); + for (const generic of unrelated) { + expect(generic.name).toBe("execute_tool view"); + expect(generic.attributes).not.toHaveProperty("gen_ai.skill.name"); + expect(generic.attributes).not.toHaveProperty("gen_ai.skill.resource.name"); + } + for (const span of [loader, reader, command]) { + expectNoLegacySkillFields(span.attributes); + expect(span.attributes["gen_ai.operation.name"]).toBe("execute_tool"); + if (captureContent) { + expect(span.attributes["gen_ai.skill.description"]).toBe( + "OTel skill description sentinel" + ); + expect( + fileURLToPath(String(span.attributes["gen_ai.skill.source.uri"])) + ).toBe(join(root, "SKILL.md")); + } else { + expect(span.attributes).not.toHaveProperty("gen_ai.skill.description"); + expect(span.attributes).not.toHaveProperty("gen_ai.skill.source.uri"); + expect(span.attributes).not.toHaveProperty("process.executable.path"); + expect(span.attributes).not.toHaveProperty("gen_ai.tool.call.arguments"); + expect(span.attributes).not.toHaveProperty("gen_ai.tool.call.result"); + } + } + for (const span of spans) { + expectNoLegacySkillFields(span.attributes); + for (const event of span.events) { + expectNoLegacySkillFields(event.attributes); + } + } + const invocation = spans + .flatMap((span) => span.events) + .find((event) => event.name === "github.copilot.skill.invoked")!; + expect(invocation.attributes["gen_ai.skill.name"]).toBe("review"); + expectNoLegacySkillFields(invocation.attributes); + expect(Boolean(invocation.attributes["github.copilot.skill.content"])).toBe( + captureContent + ); + if (captureContent) { + expect(invocation.attributes["gen_ai.skill.description"]).toBe( + "OTel skill description sentinel" + ); + expect( + fileURLToPath(String(invocation.attributes["gen_ai.skill.source.uri"])) + ).toBe(join(root, "SKILL.md")); + } else { + expect(invocation.attributes).not.toHaveProperty("gen_ai.skill.description"); + expect(invocation.attributes).not.toHaveProperty("gen_ai.skill.source.uri"); + } + } + ); + + it( + `exports unknown skill and ordinary zero and nonzero exits capture ${captureContent}`, + { timeout: 90_000 }, + async () => { + const telemetryFile = "exit-telemetry.jsonl"; + await using client = createClient({ + telemetry: { exporterType: "file", filePath: telemetryFile, captureContent }, + }); + await createSkill(workDir); + await writeFile( + join(workDir, "exit.cjs"), + "process.exit(Number(process.argv[2]));\n" + ); + const session = await client.createSession({ + enableConfigDiscovery: false, + skillDirectories: [join(workDir, "skills")], + onPermissionRequest: approveAll, + }); + const exits = new Map(); + session.on("tool.execution_complete", (event) => { + const exit = event.data.shellExecution?.exitCode; + if (exit !== undefined) { + exits.set(event.data.toolCallId, exit); + } + }); + const response = await session.sendAndWait( + { + prompt: + "Use the skill tool to attempt to load otel-nonexistent. Do not search for or install it. " + + "Then use the shell tool in sync mode to run exactly: node './exit.cjs' 0; exit 0. " + + "Then use the shell tool in sync mode to run exactly: node './exit.cjs' 7; exit 7. " + + "A nonzero exit is intentional. Do not retry it. Reply with exactly OTEL_EXIT_FLOW_DONE.", + }, + 90_000 + ); + expect(response?.data.content).toContain("OTEL_EXIT_FLOW_DONE"); + await session.disconnect(); + expect(await client.stop()).toEqual([]); + const spans = await spansFrom(join(workDir, telemetryFile)); + const loader = spans.find( + (span) => span.attributes["gen_ai.tool.name"] === "skill" + )!; + expect(loader.name).toBe("execute_tool skill otel-nonexistent"); + expect(loader.attributes["gen_ai.skill.name"]).toBe("otel-nonexistent"); + expect(loader.attributes).not.toHaveProperty("gen_ai.skill.description"); + expect(loader.attributes).not.toHaveProperty("gen_ai.skill.source.uri"); + expect(loader.status.code).toBe(2); + const commands = spans.filter( + (span) => span.attributes["gen_ai.tool.name"] === shell + ); + expect(commands).toHaveLength(2); + expect(commands.map((span) => span.attributes["process.exit.code"]).sort()).toEqual( + [0, 7] + ); + for (const command of commands) { + expectShellLauncher(command); + expect(command.name).toBe( + `execute_tool ${shell} ${command.attributes["process.executable.name"]}` + ); + expect(command.attributes).not.toHaveProperty("gen_ai.skill.name"); + expect(command.attributes).not.toHaveProperty("gen_ai.skill.resource.name"); + expect(command.attributes["process.exit.code"]).toBe( + exits.get(String(command.attributes["gen_ai.tool.call.id"])) + ); + expect(command.status.code).not.toBe(2); + } + } + ); + + it( + `keeps reloaded skill provenance and child attribution separate capture ${captureContent}`, + { timeout: 90_000 }, + async () => { + const telemetryFile = "reload-telemetry.jsonl"; + await using client = createClient({ + telemetry: { exporterType: "file", filePath: telemetryFile, captureContent }, + }); + const oldRoot = await createSkill(workDir); + const childRoot = await createSkill(workDir, "skills", "child-review"); + const session = await client.createSession({ + enableConfigDiscovery: false, + skillDirectories: [join(workDir, "skills")], + onPermissionRequest: approveAll, + }); + const childExits = new Map(); + const shellStarts: unknown[] = []; + session.on("tool.execution_start", (event) => { + if (event.data.toolName === shell) { + shellStarts.push(event.data); + } + }); + session.on("tool.execution_complete", (event) => { + const exit = event.data.shellExecution?.exitCode; + if (exit !== undefined) { + childExits.set(event.data.toolCallId, exit); + } + }); + const first = await session.sendAndWait({ + prompt: "Use the skill tool to load review, then reply with exactly OTEL_BEFORE_RELOAD_DONE.", + }); + expect(first?.data.content).toContain("OTEL_BEFORE_RELOAD_DONE"); + const newRoot = join(workDir, "skills", "moved-review"); + await rename(oldRoot, newRoot); + await writeFile( + join(newRoot, "SKILL.md"), + "---\nname: review\ndescription: Reloaded OTel description\n---\nFollow the user's explicit instructions.\n" + ); + const reload = await session.rpc.skills.reload(); + expect(reload.errors).toEqual([]); + const second = await session.sendAndWait( + { + prompt: + "Use the skill tool to load review again. Then use the task tool with agent_type exactly 'task' and mode 'sync' " + + "to load child-review with the skill tool, use view to read skills/child-review/references/policy.txt, " + + "then use the shell tool in sync mode to run exactly: node './skills/child-review/scripts/check.cjs'. " + + "The task agent should report the resource and script words. After the agent finishes, " + + "reply with exactly OTEL_AFTER_RELOAD_DONE.", + }, + 90_000 + ); + expect(second?.data.content).toContain("OTEL_AFTER_RELOAD_DONE"); + await session.disconnect(); + expect(await client.stop()).toEqual([]); + const spans = await spansFrom(join(workDir, telemetryFile)); + const loaders = spans.filter( + (span) => span.attributes["gen_ai.tool.name"] === "skill" + ); + expect(loaders).toHaveLength(3); + expect( + loaders.filter((span) => span.name === "execute_tool skill review") + ).toHaveLength(2); + expect( + loaders.map((span) => ({ + name: span.name, + agent: span.attributes["gen_ai.agent.name"], + })) + ).toContainEqual({ name: "execute_tool skill child-review", agent: "task" }); + const child = loaders.find( + (span) => span.attributes["gen_ai.agent.name"] === "task" + )!; + expect(child).toBeDefined(); + expect(child.name).toBe("execute_tool skill child-review"); + const parent = spans.find((span) => span.spanId === child.parentSpanId)!; + expect(parent.attributes["gen_ai.operation.name"]).toBe("invoke_agent"); + expect(parent.attributes["gen_ai.agent.name"]).toBe("task"); + expect(child.traceId).toBe(parent.traceId); + const childReader = spans.find( + (span) => span.name === "execute_tool view child-review references/policy.txt" + )!; + expect(childReader).toBeDefined(); + expect(childReader.parentSpanId).toBe(parent.spanId); + expect(childReader.traceId).toBe(parent.traceId); + expect(childReader.attributes["gen_ai.agent.name"]).toBe("task"); + const childCommands = spans.filter( + (span) => span.attributes["gen_ai.skill.resource.name"] === "scripts/check.cjs" + ); + expect(childCommands, JSON.stringify(shellStarts)).toHaveLength(1); + const childCommand = childCommands[0]; + expect(childCommand.name).toBe( + `execute_tool ${shell} child-review scripts/check.cjs` + ); + expect(childCommand.parentSpanId).toBe(parent.spanId); + expect(childCommand.traceId).toBe(parent.traceId); + expect(childCommand.attributes["gen_ai.agent.name"]).toBe("task"); + expectShellLauncher(childCommand); + expect(childCommand.attributes["process.exit.code"]).toBe(0); + expect(childCommand.attributes["process.exit.code"]).toBe( + childExits.get(String(childCommand.attributes["gen_ai.tool.call.id"])) + ); + if (captureContent) { + expect( + loaders + .map((span) => + fileURLToPath(String(span.attributes["gen_ai.skill.source.uri"])) + ) + .sort() + ).toEqual( + [ + join(oldRoot, "SKILL.md"), + join(newRoot, "SKILL.md"), + join(childRoot, "SKILL.md"), + ].sort() + ); + expect( + loaders.find( + (span) => + fileURLToPath( + String(span.attributes["gen_ai.skill.source.uri"]) + ) === join(oldRoot, "SKILL.md") + )!.attributes["gen_ai.skill.description"] + ).toBe("OTel skill description sentinel"); + expect( + loaders.find( + (span) => + fileURLToPath( + String(span.attributes["gen_ai.skill.source.uri"]) + ) === join(newRoot, "SKILL.md") + )!.attributes["gen_ai.skill.description"] + ).toBe("Reloaded OTel description"); + } + } + ); + }); +} + +describe("Preloaded skill telemetry", async () => { + const telemetryFile = "preload-telemetry.jsonl"; + const { createClient, workDir } = await createSdkTestContext({ + copilotClientOptions: { + connection: RuntimeConnection.forStdio(), + }, + }); + it("emits an invocation receipt without fabricating an execute tool span", async () => { + await using client = createClient({ + telemetry: { exporterType: "file", filePath: telemetryFile, captureContent: true }, + }); + const root = await createSkill(workDir); + const session = await client.createSession({ + enableConfigDiscovery: false, + skillDirectories: [join(workDir, "skills")], + customAgents: [ + { + name: "preloaded-review", + description: "Preload review", + prompt: "Follow the user.", + skills: ["review"], + }, + ], + agent: "preloaded-review", + onPermissionRequest: approveAll, + }); + const response = await session.sendAndWait({ + prompt: "Do not use any tools. Report the verification word from your preloaded review skill and then OTEL_PRELOAD_DONE.", + }); + expect(response?.data.content).toContain("OTEL_PRELOAD_DONE"); + expect(response?.data.content).toContain("OTEL_SKILL_ORCHID"); + await session.disconnect(); + expect(await client.stop()).toEqual([]); + const spans = await spansFrom(join(workDir, telemetryFile)); + expect( + spans.filter((span) => span.attributes["gen_ai.operation.name"] === "execute_tool") + ).toHaveLength(0); + const agent = spans.find( + (span) => span.attributes["gen_ai.operation.name"] === "invoke_agent" + )!; + expect(agent.attributes["gen_ai.agent.name"]).toBe("preloaded-review"); + const invocations = agent.events.filter( + (event) => event.name === "github.copilot.skill.invoked" + ); + expect(invocations).toHaveLength(1); + const invocation = invocations[0]; + expect(invocation.attributes["gen_ai.skill.name"]).toBe("review"); + expect(invocation.attributes["gen_ai.skill.description"]).toBe( + "OTel skill description sentinel" + ); + expect(fileURLToPath(String(invocation.attributes["gen_ai.skill.source.uri"]))).toBe( + join(root, "SKILL.md") + ); + expect(invocation.attributes["github.copilot.skill.content"]).toContain( + "OTEL_SKILL_ORCHID" + ); + expectNoLegacySkillFields(invocation.attributes); + }); +}); diff --git a/nodejs/test/e2e/telemetry.e2e.test.ts b/nodejs/test/e2e/telemetry.e2e.test.ts index ddcae643e3..49d7e66a26 100644 --- a/nodejs/test/e2e/telemetry.e2e.test.ts +++ b/nodejs/test/e2e/telemetry.e2e.test.ts @@ -7,7 +7,7 @@ import { join } from "path"; import { describe, expect, it } from "vitest"; import { z } from "zod"; import { approveAll, defineTool, RuntimeConnection } from "../../src/index.js"; -import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { createSdkTestContext, DEFAULT_GITHUB_TOKEN } from "./harness/sdkTestContext.js"; interface TelemetryEntry { type?: string; @@ -61,6 +61,7 @@ describe("Telemetry export", async () => { workDir, } = await createSdkTestContext({ copilotClientOptions: { + gitHubToken: DEFAULT_GITHUB_TOKEN, // Telemetry is lowered to environment variables the native runtime reads, which // the in-process transport cannot carry per-client (the runtime runs in the shared // host process); see https://github.com/github/copilot-sdk/issues/1934. Pin the @@ -77,6 +78,8 @@ describe("Telemetry export", async () => { }); it("should export file telemetry for sdk interactions", { timeout: 90_000 }, async () => { + await client.start(); + expect((await client.getAuthStatus()).isAuthenticated).toBe(true); const session = await client.createSession({ onPermissionRequest: approveAll, tools: [ @@ -88,6 +91,11 @@ describe("Telemetry export", async () => { ], }); + // Resolve account metadata before the first invocation snapshots its identity. + expect(await session.rpc.gitHubAuth.getStatus()).toMatchObject({ + isAuthenticated: true, + login: "e2e-test-user", + }); const assistantMessage = await session.sendAndWait({ prompt }, 90_000); expect(assistantMessage).toBeDefined(); expect(assistantMessage?.data.content ?? "").toContain("TELEMETRY_E2E_DONE"); @@ -117,6 +125,10 @@ describe("Telemetry export", async () => { session.sessionId ); expect(isRootSpan(invokeAgentSpan!)).toBe(true); + expect(getStringAttribute(invokeAgentSpan!, "enduser.pseudo.id")).toBe( + "e2e-test-tracking-id" + ); + expect(invokeAgentSpan!.attributes).not.toHaveProperty("user.name"); const invokeAgentSpanId = invokeAgentSpan!.spanId; expect(invokeAgentSpanId).toBeTruthy(); const invokeAgentTraceId = invokeAgentSpan!.traceId; @@ -135,6 +147,23 @@ describe("Telemetry export", async () => { (getStringAttribute(span, "gen_ai.input.messages") ?? "").includes(prompt) ) ).toBe(true); + + const toolDefinitions = chatSpans.flatMap((span) => { + const definitions = getStringAttribute(span, "gen_ai.tool.definitions"); + return definitions ? (JSON.parse(definitions) as unknown[]) : []; + }); + expect(toolDefinitions).toContainEqual( + expect.objectContaining({ + name: toolName, + description: "Echoes a marker string for telemetry validation.", + parameters: expect.objectContaining({ + type: "object", + properties: expect.objectContaining({ + value: expect.objectContaining({ type: "string" }), + }), + }), + }) + ); expect( chatSpans.some((span) => (getStringAttribute(span, "gen_ai.output.messages") ?? "").includes( @@ -166,6 +195,7 @@ describe("Telemetry export", async () => { const subagentClient = createClient({ connection: RuntimeConnection.forStdio(), + gitHubToken: DEFAULT_GITHUB_TOKEN, telemetry: { filePath: telemetryFileName, exporterType: "file", @@ -175,9 +205,12 @@ describe("Telemetry export", async () => { }); let sessionId: string; try { + await subagentClient.start(); + expect((await subagentClient.getAuthStatus()).isAuthenticated).toBe(true); const session = await subagentClient.createSession({ onPermissionRequest: approveAll }); sessionId = session.sessionId; try { + expect((await session.rpc.gitHubAuth.getStatus()).isAuthenticated).toBe(true); const response = await session.sendAndWait({ prompt }, 90_000); expect(response?.data.content ?? "").toContain("SUBAGENT_OTEL_DONE"); } finally { @@ -196,6 +229,10 @@ describe("Telemetry export", async () => { (span) => getStringAttribute(span, "gen_ai.operation.name") === "invoke_agent" ); expect(invocationSpans).toHaveLength(2); + for (const span of invocationSpans) { + expect(getStringAttribute(span, "enduser.pseudo.id")).toBe("e2e-test-tracking-id"); + expect(span.attributes).not.toHaveProperty("user.name"); + } const roots = invocationSpans.filter(isRootSpan); expect(roots).toHaveLength(1); const root = roots[0]!; diff --git a/nodejs/test/e2e/telemetry_http.e2e.test.ts b/nodejs/test/e2e/telemetry_http.e2e.test.ts new file mode 100644 index 0000000000..7ffc613899 --- /dev/null +++ b/nodejs/test/e2e/telemetry_http.e2e.test.ts @@ -0,0 +1,257 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { randomUUID } from "node:crypto"; +import { createServer } from "node:http"; +import type { Server } from "node:http"; +import { join } from "node:path"; +import { describe, it } from "vitest"; +import { z } from "zod"; +import { approveAll, defineTool, RuntimeConnection } from "../../src/index.js"; +import type { CopilotClient } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +const attribute = z.object({ + key: z.string(), + value: z.object({ stringValue: z.string().optional() }).passthrough(), +}); +const tracePayload = z.object({ + resourceSpans: z.array( + z.object({ + scopeSpans: z.array( + z.object({ + scope: z.object({ name: z.string() }), + spans: z.array( + z + .object({ + traceId: z.string(), + spanId: z.string(), + attributes: z.array(attribute), + }) + .passthrough() + ), + }) + ), + }) + ), +}); +const metricPayload = z.object({ + resourceMetrics: z.array( + z.object({ + scopeMetrics: z.array( + z.object({ + scope: z.object({ name: z.string() }), + metrics: z.array(z.object({ name: z.string() }).passthrough()), + }) + ), + }) + ), +}); +const exportRecord = z.object({ path: z.string(), payload: z.unknown() }); +type ExportRecord = z.infer; + +async function closeServer(server: Server): Promise { + server.closeAllConnections(); + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ); +} + +describe("HTTP telemetry export", async () => { + // The KDC workflow runs this same public SDK scenario through real Squid + // authentication. Ordinary SDK CI uses a test-owned loopback collector. + const proxy = process.env.COPILOT_KDC_PROXY_URL; + const collectorUrl = process.env.COPILOT_KDC_COLLECTOR_URL; + const ca = process.env.COPILOT_KDC_CA_PATH; + if (proxy && (!collectorUrl || !ca)) { + throw new Error("KDC telemetry E2E requires collector URL and CA path"); + } + const caseId = randomUUID(); + const harness = await createSdkTestContext({ + copilotClientOptions: { + connection: RuntimeConnection.forStdio(), + env: { + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "", + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "", + OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: "", + OTEL_EXPORTER_OTLP_METRICS_PROTOCOL: "", + OTEL_EXPORTER_OTLP_CERTIFICATE: "", + OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE: "", + OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE: "", + OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE: "", + OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE: "", + OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE: "", + OTEL_EXPORTER_OTLP_CLIENT_KEY: "", + OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY: "", + OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY: "", + OTEL_EXPORTER_OTLP_TIMEOUT: "10000", + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: "", + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: "", + OTEL_EXPORTER_OTLP_TRACES_HEADERS: `x-test-case=${caseId}`, + OTEL_EXPORTER_OTLP_METRICS_HEADERS: `x-test-case=${caseId}`, + OTEL_EXPORTER_OTLP_HEADERS: `x-test-case=${caseId}`, + NO_PROXY: "127.0.0.1,localhost,::1", + no_proxy: "127.0.0.1,localhost,::1", + ...(proxy + ? { + HTTP_PROXY: proxy, + http_proxy: proxy, + HTTPS_PROXY: proxy, + https_proxy: proxy, + NODE_EXTRA_CA_CERTS: ca, + SSL_CERT_FILE: ca, + CURL_CA_BUNDLE: ca, + COPILOT_PROXY_KERBEROS_SPN: "HTTP/proxy.example.test", + } + : {}), + }, + }, + }); + + it( + "exports completed SDK turns as OTLP traces and metrics", + { timeout: 90_000 }, + async ({ expect, onTestFinished }) => { + const received: ExportRecord[] = []; + const failures: Error[] = []; + let client: CopilotClient | undefined; + let startingServer: Promise | undefined; + onTestFinished(async () => { + try { + if (client) { + expect(await client.stop()).toEqual([]); + } + } finally { + const [startup] = await Promise.allSettled( + startingServer ? [startingServer] : [] + ); + if (startup?.status === "fulfilled") { + await closeServer(startup.value); + } + } + }); + let endpoint = "https://kerberos-proxy-test.invalid:8443"; + if (!proxy) { + const server = createServer((request, response) => { + void (async () => { + expect(request.headers["x-test-case"]).toBe(caseId); + const chunks: Buffer[] = []; + for await (const chunk of request) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + received.push({ + path: request.url ?? "", + payload: JSON.parse(Buffer.concat(chunks).toString("utf8")), + }); + response.setHeader("content-type", "application/json"); + response.end("{}"); + })().catch((error: Error) => { + failures.push(error); + response.writeHead(500).end(); + }); + }); + startingServer = new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => resolve(server)); + }); + await startingServer; + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("OTLP collector did not bind a TCP address"); + } + endpoint = `http://127.0.0.1:${address.port}`; + } + + // Reuse the existing telemetry turn capture, unchanged: the transport + // test does not need another model sample or handcrafted response. + await harness.openAiEndpoint.updateConfig({ + filePath: join( + import.meta.dirname, + "..", + "..", + "..", + "test", + "snapshots", + "telemetry", + "should_export_file_telemetry_for_sdk_interactions.yaml" + ), + workDir: harness.workDir, + }); + const sourceName = "ts-sdk-telemetry-e2e"; + client = harness.createClient({ + telemetry: { + exporterType: "otlp-http", + otlpEndpoint: endpoint, + otlpProtocol: "http/json", + sourceName, + captureContent: true, + }, + }); + const session = await client.createSession({ + onPermissionRequest: approveAll, + tools: [ + defineTool("echo_telemetry_marker", { + description: "Echoes a marker string for telemetry validation.", + parameters: z.object({ value: z.string() }), + handler: ({ value }) => value, + }), + ], + }); + const assistant = await session.sendAndWait( + { + prompt: "Use the echo_telemetry_marker tool with value 'copilot-sdk-telemetry-e2e', then respond with TELEMETRY_E2E_DONE.", + }, + 90_000 + ); + expect(assistant?.data.content).toContain("TELEMETRY_E2E_DONE"); + await session.disconnect(); + expect(await client.stop()).toEqual([]); + + if (proxy) { + const response = await fetch(`${collectorUrl}/exports/${caseId}`); + expect(response.ok).toBe(true); + received.push(...z.array(exportRecord).parse(await response.json())); + } + expect(failures).toEqual([]); + const traces = received + .filter(({ path }) => path === "/v1/traces") + .flatMap(({ payload }) => tracePayload.parse(payload).resourceSpans) + .flatMap(({ scopeSpans }) => scopeSpans); + const metrics = received + .filter(({ path }) => path === "/v1/metrics") + .flatMap(({ payload }) => metricPayload.parse(payload).resourceMetrics) + .flatMap(({ scopeMetrics }) => scopeMetrics); + expect(traces.length).toBeGreaterThan(0); + expect(metrics.length).toBeGreaterThan(0); + expect(traces.every(({ scope }) => scope.name === sourceName)).toBe(true); + expect(metrics.every(({ scope }) => scope.name === sourceName)).toBe(true); + const spans = traces.flatMap(({ spans }) => spans); + expect( + spans.some(({ attributes }) => + attributes.some( + ({ key, value }) => + key === "gen_ai.conversation.id" && + value.stringValue === session.sessionId + ) + ) + ).toBe(true); + expect( + spans.some(({ attributes }) => + attributes.some( + ({ key, value }) => + key === "gen_ai.output.messages" && + value.stringValue?.includes("TELEMETRY_E2E_DONE") + ) + ) + ).toBe(true); + expect( + spans.every( + ({ traceId, spanId }) => + /^[0-9a-f]{32}$/i.test(traceId) && /^[0-9a-f]{16}$/i.test(spanId) + ) + ).toBe(true); + expect(metrics.flatMap(({ metrics }) => metrics).length).toBeGreaterThan(0); + } + ); +}); diff --git a/nodejs/test/e2e/tools.e2e.test.ts b/nodejs/test/e2e/tools.e2e.test.ts index 16db1aca87..78d18c0ca9 100644 --- a/nodejs/test/e2e/tools.e2e.test.ts +++ b/nodejs/test/e2e/tools.e2e.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { writeFile } from "fs/promises"; +import { access, writeFile } from "fs/promises"; import { join } from "path"; import { assert, describe, expect, it } from "vitest"; import { z } from "zod"; @@ -45,6 +45,51 @@ describe("Custom tools", async () => { expect(assistantMessage?.data.content).toContain("HELLO"); }); + it("updates the calling connection's tool definitions through the public session RPC", async () => { + const original = { + name: "sdk_epoch_original", + description: "The original SDK-owned tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const replacement = { + name: "sdk_epoch_replacement", + description: "A replacement SDK-owned tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const session = await client.createSession({ + onPermissionRequest: approveAll, + tools: [ + defineTool(original.name, { + description: original.description, + handler: () => "ORIGINAL_TOOL_RESULT", + }), + ], + }); + try { + const currentToolNames = async () => { + await session.rpc.tools.initializeAndValidate(); + return (await session.rpc.tools.getCurrentMetadata()).tools.map( + (tool) => tool.name + ); + }; + expect(await currentToolNames()).toContain(original.name); + + await session.rpc.tools.set({ tools: [original, replacement] }); + expect(await currentToolNames()).toEqual( + expect.arrayContaining([original.name, replacement.name]) + ); + + await session.rpc.tools.set({ tools: [replacement] }); + const afterRemoval = await currentToolNames(); + expect(afterRemoval).toContain(replacement.name); + expect(afterRemoval).not.toContain(original.name); + } finally { + await session.disconnect(); + } + }); + // TODO(cli-1.0.81-2): CLI 1.0.81-2 stops servicing nested requests on the same stdio // connection while awaiting a tool handler, so calling session.history.clearContext // from inside a terminal tool deadlocks. Tracked as a runtime reentrancy regression; @@ -299,6 +344,104 @@ describe("Custom tools", async () => { expect(assistantMessage?.data.content?.toLowerCase()).toMatch(/hello|search|found/); }); + it("string schema apply_patch override binds patch input", async () => { + const patch = + "*** Begin Patch\n*** Add File: override-marker.txt\n+from-native\n*** End Patch"; + let receivedPatch: string | undefined; + let receivedArguments: unknown; + const session = await client.createSession({ + onPermissionRequest: approveAll, + tools: [ + defineTool("apply_patch", { + parameters: z.string(), + overridesBuiltInTool: true, + handler: (input, invocation) => { + receivedPatch = input; + receivedArguments = invocation.arguments; + return "HOST_PATCH_HANDLED"; + }, + }), + ], + }); + + const response = await session.sendAndWait({ + prompt: "Use apply_patch to apply the supplied patch.", + }); + + expect(receivedPatch).toBe(patch); + expect(receivedArguments).toBe(patch); + expect(response?.data.content).toContain("Host override completed."); + await expect(access(join(workDir, "override-marker.txt"))).rejects.toMatchObject({ + code: "ENOENT", + }); + const traffic = await openAiEndpoint.getExchanges(); + const tool = traffic[0]?.request.tools?.find( + (entry) => entry.type === "function" && entry.function.name === "apply_patch" + ); + expect(tool?.function.parameters).toMatchObject({ + type: "object", + properties: { input: { type: "string" } }, + required: ["input"], + }); + expect( + traffic[1]?.request.messages.some( + (message) => message.role === "tool" && message.content === "HOST_PATCH_HANDLED" + ) + ).toBe(true); + }); + + it("reports rejected apply_patch string schema to the SDK", async () => { + const warnings: string[] = []; + const session = await client.createSession({ + onPermissionRequest: approveAll, + onEvent: (event) => { + if (event.type === "session.warning") { + warnings.push(event.data.message); + } + }, + tools: [ + defineTool("apply_patch", { + parameters: { + type: "string", + $defs: { patch: { type: "string" } }, + $ref: "#/$defs/patch", + }, + overridesBuiltInTool: true, + handler: () => "unexpected override call", + }), + defineTool("valid_sibling", { + parameters: z.object({}), + handler: () => "valid", + }), + defineTool("invalid_sibling", { + parameters: { type: "number" }, + handler: () => "unexpected invalid tool call", + }), + ], + }); + + await session.rpc.mode.set({ mode: "plan" }); + await session.rpc.mode.set({ mode: "interactive" }); + const response = await session.sendAndWait({ + prompt: "Reply with exactly Session ready.", + }); + expect(response?.data.content).toContain("Session ready."); + expect(warnings).toEqual([ + 'External tool "apply_patch" has invalid parameters schema (apply_patch string overrides cannot contain JSON Schema references), ignoring tool', + ]); + const traffic = await openAiEndpoint.getExchanges(); + expect( + traffic[0]?.request.tools?.some( + (tool) => tool.type === "function" && tool.function.name === "valid_sibling" + ) + ).toBe(true); + expect( + traffic[0]?.request.tools?.some( + (tool) => tool.type === "function" && tool.function.name === "invalid_sibling" + ) + ).toBe(false); + }); + it("denies custom tool when permission denied", async () => { let toolHandlerCalled = false; diff --git a/nodejs/test/e2e/tools_anthropic_updates.e2e.test.ts b/nodejs/test/e2e/tools_anthropic_updates.e2e.test.ts new file mode 100644 index 0000000000..721b432ae1 --- /dev/null +++ b/nodejs/test/e2e/tools_anthropic_updates.e2e.test.ts @@ -0,0 +1,411 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +import { + approveAll, + CopilotRequestHandler, + defineTool, + type CopilotRequestContext, +} from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; + +interface AnthropicRequest { + system: unknown; + tools: unknown; + messages: Array<{ role: string; content: unknown }>; +} + +function streamResponse(block: Record, stopReason: string): Response { + const events: Array<[string, Record]> = [ + [ + "message_start", + { + type: "message_start", + message: { + id: "msg_sdk_tool_update", + type: "message", + role: "assistant", + model: "claude-opus-5.5", + content: [], + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 5, output_tokens: 1 }, + }, + }, + ], + ["content_block_start", { type: "content_block_start", index: 0, content_block: block }], + ["content_block_stop", { type: "content_block_stop", index: 0 }], + [ + "message_delta", + { + type: "message_delta", + delta: { stop_reason: stopReason, stop_sequence: null }, + usage: { output_tokens: 7 }, + }, + ], + ["message_stop", { type: "message_stop" }], + ]; + return new Response( + events + .map(([event, data]) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`) + .join(""), + { headers: { "content-type": "text/event-stream" } } + ); +} + +function streamRejectionAfterOutput(): Response { + const events = [ + [ + "message_start", + { + type: "message_start", + message: { + id: "msg_sdk_tool_rejected", + type: "message", + role: "assistant", + model: "claude-opus-5.5", + content: [], + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 5, output_tokens: 1 }, + }, + }, + ], + [ + "content_block_start", + { + type: "content_block_start", + index: 0, + content_block: { type: "text", text: "" }, + }, + ], + [ + "content_block_delta", + { + type: "content_block_delta", + index: 0, + delta: { type: "text_delta", text: "already streamed" }, + }, + ], + [ + "error", + { + type: "error", + error: { type: "invalid_request_error", message: "invalid tool_addition" }, + }, + ], + ] as const; + return new Response( + events + .map(([event, data]) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`) + .join(""), + { headers: { "content-type": "text/event-stream" } } + ); +} + +class AnthropicToolUpdateHandler extends CopilotRequestHandler { + readonly requests: Array<{ body: AnthropicRequest; beta: string | null }> = []; + + constructor( + private readonly rejectPositionedUpdate: false | "before-output" | "after-output" = false + ) { + super(); + } + + protected override async sendRequest( + request: Request, + ctx: CopilotRequestContext + ): Promise { + if (new URL(request.url).hostname !== "api.anthropic.com") { + return super.sendRequest(request, ctx); + } + const body = (await request.json()) as AnthropicRequest; + const serialized = JSON.stringify(body.messages); + if ( + !serialized.includes("SDK_ANTHROPIC_FIRST") && + !serialized.includes("SDK_ANTHROPIC_AFTER") && + !serialized.includes("SDK_ANTHROPIC_FOLLOW_UP") + ) { + return streamResponse({ type: "text", text: "Unrelated request" }, "end_turn"); + } + this.requests.push({ body, beta: request.headers.get("anthropic-beta") }); + + if (serialized.includes("SDK_ANTHROPIC_FOLLOW_UP")) { + return streamResponse({ type: "text", text: "SDK_ANTHROPIC_LEGACY_OK" }, "end_turn"); + } + if ( + body.messages.some( + (message) => + Array.isArray(message.content) && + message.content.some((block: { type?: string }) => block.type === "tool_result") + ) + ) { + return streamResponse({ type: "text", text: "SDK_ANTHROPIC_TOOL_OK" }, "end_turn"); + } + if (serialized.includes("SDK_ANTHROPIC_AFTER")) { + if (this.rejectPositionedUpdate) { + if (serialized.includes('"type":"tool_addition"')) { + if (this.rejectPositionedUpdate === "after-output") { + return streamRejectionAfterOutput(); + } + return new Response( + JSON.stringify({ + error: { + type: "invalid_request_error", + message: "invalid tool_addition", + }, + }), + { status: 400, headers: { "content-type": "application/json" } } + ); + } + return streamResponse( + { type: "text", text: "SDK_ANTHROPIC_RECOVERED" }, + "end_turn" + ); + } + return streamResponse( + { type: "tool_use", id: "toolu_sdk_added", name: "sdk_epoch_added", input: {} }, + "tool_use" + ); + } + return streamResponse({ type: "text", text: "SDK_ANTHROPIC_FIRST_OK" }, "end_turn"); + } +} + +describe("SDK tool updates on direct Anthropic Messages", async () => { + const handler = new AnthropicToolUpdateHandler(); + const { copilotClient: client } = await createSdkTestContext({ + copilotClientOptions: { requestHandler: handler }, + }); + + it("keeps the top-level prefix and executes a newly added inline tool", async () => { + let addedToolCalls = 0; + const session = await client.createSession({ + model: "claude-opus-5.5", + toolSearch: { enabled: false }, + onPermissionRequest: approveAll, + provider: { + type: "anthropic", + baseUrl: "https://api.anthropic.com", + apiKey: "synthetic-provider-key", + modelId: "claude-opus-5.5", + wireModel: "claude-opus-5.5", + }, + tools: [ + defineTool("sdk_epoch_original", { + description: "Original SDK tool", + handler: () => "original", + }), + defineTool("sdk_epoch_added", { + description: "Added SDK tool", + handler: () => { + addedToolCalls++; + return "SDK_ADDED_TOOL_RESULT"; + }, + }), + ], + }); + try { + await session.rpc.tools.initializeAndValidate(); + expect( + (await session.rpc.tools.getCurrentMetadata()).tools.map((tool) => tool.name) + ).toEqual(expect.arrayContaining(["sdk_epoch_original", "sdk_epoch_added"])); + const original = { + name: "sdk_epoch_original", + description: "Original SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const added = { + name: "sdk_epoch_added", + description: "Added SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + await session.rpc.tools.set({ tools: [original] }); + + const first = await session.sendAndWait({ prompt: "SDK_ANTHROPIC_FIRST" }); + expect(first?.data.content).toContain("SDK_ANTHROPIC_FIRST_OK"); + await session.rpc.tools.set({ tools: [original, added] }); + const second = await session.sendAndWait({ + prompt: "SDK_ANTHROPIC_AFTER: use sdk_epoch_added now.", + }); + expect(second?.data.content).toContain("SDK_ANTHROPIC_TOOL_OK"); + expect(addedToolCalls).toBe(1); + + expect(handler.requests).toHaveLength(3); + const [before, update, afterTool] = handler.requests; + expect(update.body.system).toEqual(before.body.system); + expect(update.body.tools).toEqual(before.body.tools); + expect(afterTool.body.system).toEqual(before.body.system); + expect(afterTool.body.tools).toEqual(before.body.tools); + const blocks = update.body.messages.flatMap((message) => + Array.isArray(message.content) ? message.content : [] + ); + expect(blocks).toContainEqual( + expect.objectContaining({ + type: "tool_addition", + tool: expect.objectContaining({ + type: "tool_definition", + definition: expect.objectContaining({ name: "sdk_epoch_added" }), + }), + }) + ); + expect(update.beta).toContain("inline-tools-2026-09-15"); + expect(update.beta).not.toContain("mid-conversation-tool-changes-2026-07-01"); + expect(JSON.stringify(afterTool.body.messages)).toContain("SDK_ADDED_TOOL_RESULT"); + } finally { + await session.disconnect(); + } + }); +}); + +describe("SDK tool updates on direct Anthropic Messages", async () => { + const rejectingHandler = new AnthropicToolUpdateHandler("before-output"); + const { copilotClient: retryClient } = await createSdkTestContext({ + copilotClientOptions: { requestHandler: rejectingHandler }, + }); + + it("retries a rejected inline update once with the full current tools", async () => { + const original = { + name: "sdk_epoch_original", + description: "Original SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const added = { + name: "sdk_epoch_added", + description: "Added SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const session = await retryClient.createSession({ + model: "claude-opus-5.5", + toolSearch: { enabled: false }, + onPermissionRequest: approveAll, + provider: { + type: "anthropic", + baseUrl: "https://api.anthropic.com", + apiKey: "synthetic-provider-key", + modelId: "claude-opus-5.5", + wireModel: "claude-opus-5.5", + }, + tools: [ + defineTool(original.name, { + description: original.description, + handler: () => "original", + }), + defineTool(added.name, { description: added.description, handler: () => "added" }), + ], + }); + try { + await session.rpc.tools.initializeAndValidate(); + await session.rpc.tools.set({ tools: [original] }); + const first = await session.sendAndWait({ prompt: "SDK_ANTHROPIC_FIRST" }); + expect(first?.data.content).toContain("SDK_ANTHROPIC_FIRST_OK"); + + await session.rpc.tools.set({ tools: [original, added] }); + const recovered = await session.sendAndWait({ + prompt: "SDK_ANTHROPIC_AFTER: add a tool.", + }); + expect(recovered?.data.content).toContain("SDK_ANTHROPIC_RECOVERED"); + expect(rejectingHandler.requests).toHaveLength(3); + const [before, rejected, retry] = rejectingHandler.requests; + expect(rejected.body.system).toEqual(before.body.system); + expect(rejected.body.tools).toEqual(before.body.tools); + expect(JSON.stringify(rejected.body.messages)).toContain('"type":"tool_addition"'); + expect(rejected.beta).toContain("inline-tools-2026-09-15"); + expect(retry.body.system).toEqual(before.body.system); + expect((retry.body.tools as Array<{ name: string }>).map((tool) => tool.name)).toEqual( + expect.arrayContaining([original.name, added.name]) + ); + expect(JSON.stringify(retry.body.messages)).not.toContain('"type":"tool_addition"'); + expect(retry.beta).not.toContain("inline-tools-2026-09-15"); + } finally { + await session.disconnect(); + } + }); +}); + +describe("SDK tool updates after a streamed Anthropic rejection", async () => { + const handler = new AnthropicToolUpdateHandler("after-output"); + const { copilotClient: client } = await createSdkTestContext({ + copilotClientOptions: { requestHandler: handler }, + }); + + it("disables the rejected epoch without replaying output and uses full tools next turn", async () => { + const original = { + name: "sdk_epoch_original", + description: "Original SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const added = { + name: "sdk_epoch_added", + description: "Added SDK tool", + parameters: { type: "object", properties: {} }, + defer: "never", + } as const; + const session = await client.createSession({ + model: "claude-opus-5.5", + toolSearch: { enabled: false }, + onPermissionRequest: approveAll, + provider: { + type: "anthropic", + baseUrl: "https://api.anthropic.com", + apiKey: "synthetic-provider-key", + modelId: "claude-opus-5.5", + wireModel: "claude-opus-5.5", + }, + tools: [ + defineTool(original.name, { + description: original.description, + handler: () => "original", + }), + defineTool(added.name, { description: added.description, handler: () => "added" }), + ], + }); + let failedTurnIdle = false; + const unsubscribe = session.on((event) => { + if (event.type === "session.idle" && !event.agentId) { + failedTurnIdle = true; + } + }); + try { + await session.rpc.tools.initializeAndValidate(); + await session.rpc.tools.set({ tools: [original] }); + const first = await session.sendAndWait({ prompt: "SDK_ANTHROPIC_FIRST" }); + expect(first?.data.content).toContain("SDK_ANTHROPIC_FIRST_OK"); + failedTurnIdle = false; + + await session.rpc.tools.set({ tools: [original, added] }); + await expect( + session.sendAndWait({ prompt: "SDK_ANTHROPIC_AFTER: add a tool." }) + ).rejects.toThrow(/invalid tool_addition/); + await waitForCondition(() => failedTurnIdle, { + timeoutMessage: "Rejected SDK tool update did not reach session.idle", + }); + expect(handler.requests).toHaveLength(2); + const [before, rejected] = handler.requests; + expect(rejected.body.system).toEqual(before.body.system); + expect(rejected.body.tools).toEqual(before.body.tools); + expect(JSON.stringify(rejected.body.messages)).toContain('"type":"tool_addition"'); + + const followUp = await session.sendAndWait({ prompt: "SDK_ANTHROPIC_FOLLOW_UP" }); + expect(followUp?.data.content).toContain("SDK_ANTHROPIC_LEGACY_OK"); + expect(handler.requests).toHaveLength(3); + const request = handler.requests[2]; + expect( + (request.body.tools as Array<{ name: string }>).map((tool) => tool.name) + ).toEqual(expect.arrayContaining([original.name, added.name])); + expect(JSON.stringify(request.body.messages)).not.toContain('"type":"tool_addition"'); + expect(request.beta).not.toContain("inline-tools-2026-09-15"); + } finally { + unsubscribe(); + await session.disconnect(); + } + }); +}); diff --git a/nodejs/test/e2e/transcript_recovery.e2e.test.ts b/nodejs/test/e2e/transcript_recovery.e2e.test.ts new file mode 100644 index 0000000000..61c2bf4176 --- /dev/null +++ b/nodejs/test/e2e/transcript_recovery.e2e.test.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { randomUUID } from "node:crypto"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { approveAll, type TranscriptRecovery } from "../../src/index.js"; +import { createSdkTestContext, isInProcessTransport } from "./harness/sdkTestContext.js"; + +describe.skipIf(isInProcessTransport)("Transcript recovery through the public SDK", async () => { + const { createClient, workDir } = await createSdkTestContext(); + + it.each([ + ["empty", "torn final record"], + ["copilot-cli", "torn final record"], + ["empty", "forward-compatible record before session.start"], + ["copilot-cli", "forward-compatible record before session.start"], + ] as const)( + "uses permissive defaults in %s mode for a %s and supports explicit rejection", + { timeout: 90_000 }, + async (mode, damage) => { + // Stop the log-writing client before fixture afterEach removes workDir. + await using client = createClient({ mode, baseDirectory: workDir }); + + const sessionId = randomUUID(); + const sessionDir = join(workDir, "session-state", sessionId); + const eventsPath = join(sessionDir, "events.jsonl"); + const timestamp = "2024-01-02T03:04:05.000Z"; + const intact = `${JSON.stringify({ + id: randomUUID(), + parentId: null, + type: "session.start", + timestamp, + data: { + sessionId, + version: 1, + producer: "copilot-agent", + copilotVersion: "0.0.353", + startTime: timestamp, + }, + })}\n`; + await mkdir(sessionDir, { recursive: true }); + await writeFile(eventsPath, intact); + await writeFile( + join(sessionDir, "workspace.yaml"), + JSON.stringify({ + id: sessionId, + cwd: workDir, + summary_count: 0, + created_at: timestamp, + updated_at: timestamp, + }) + ); + const tornTail = damage === "torn final record"; + const damaged = tornTail + ? intact + '{"type":"user.message","data":' + : `${JSON.stringify({ id: randomUUID(), type: "newer.event", data: {} })}\n${intact}`; + const expectedRecovery = { + invalidLineNumbers: tornTail ? [2] : [], + sessionStartMoved: !tornTail, + }; + await writeFile(eventsPath, damaged); + + await expect( + client.resumeSession(sessionId, { + availableTools: ["builtin:ask_user"], + onPermissionRequest: approveAll, + allowTranscriptRecovery: false, + }) + ).rejects.toMatchObject({ + code: -32075, + data: expectedRecovery, + }); + expect(await readFile(eventsPath, "utf8")).toBe(damaged); + + const resumed = await client.resumeSession(sessionId, { + availableTools: ["builtin:ask_user"], + onPermissionRequest: approveAll, + }); + const recovery: TranscriptRecovery | undefined = resumed.transcriptRecovery; + expect(recovery).toMatchObject(expectedRecovery); + expect(recovery?.plannedBackupPath).toContain("events.jsonl.backup-before-recovery-"); + await resumed.disconnect(); + } + ); +}); diff --git a/nodejs/test/e2e/ui_elicitation.e2e.test.ts b/nodejs/test/e2e/ui_elicitation.e2e.test.ts index 51195206aa..4287b4686b 100644 --- a/nodejs/test/e2e/ui_elicitation.e2e.test.ts +++ b/nodejs/test/e2e/ui_elicitation.e2e.test.ts @@ -10,6 +10,7 @@ import { getLegacyCliPathForTests, isInProcessTransport, } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; describe("UI Elicitation", async () => { const { copilotClient: client } = await createSdkTestContext(); @@ -29,6 +30,74 @@ describe("UI Elicitation Callback", async () => { const ctx = await createSdkTestContext(); const client = ctx.copilotClient; + it( + "automated SDK ask_user callback settles without trusted human authority", + { timeout: 60_000 }, + async () => { + const events: SessionEvent[] = []; + let receivedMessage: string | undefined; + const session = await client.createSession({ + onPermissionRequest: approveAll, + askUserVariant: "elicitation", + onElicitationRequest: async (request) => { + receivedMessage = request.message; + return { + action: "accept", + content: { answer: "github/st-mcp-poc" }, + }; + }, + }); + const unsubscribe = session.on((event) => { + events.push(event); + }); + try { + const response = await session.sendAndWait({ + prompt: + "Invoke the ask_user tool now. Set message to exactly 'Which repository should I inspect?' " + + "and request an object with one required string field named answer. Do not describe the tool call. " + + "After the tool returns, reply with exactly SDK_AUTOMATED_ASK_USER_DONE.", + }); + await waitForCondition( + () => + events.some((event) => event.type === "elicitation.completed") && + events.some((event) => event.type === "tool.execution_complete"), + { + timeoutMs: 60_000, + timeoutMessage: `Timed out waiting for ask_user completion; events: ${events + .map((event) => event.type) + .join(", ")}`, + } + ); + const elicitationEvent = events.find( + (event) => event.type === "elicitation.completed" + ); + const toolEvent = events.find((event) => event.type === "tool.execution_complete"); + + expect(receivedMessage).toBe("Which repository should I inspect?"); + expect(elicitationEvent).toMatchObject({ + type: "elicitation.completed", + data: { + action: "accept", + content: { answer: "github/st-mcp-poc" }, + }, + }); + expect(toolEvent).toMatchObject({ + type: "tool.execution_complete", + data: { success: true }, + }); + expect(events.some((event) => event.type === "elicitation.requested")).toBe(true); + expect(events.some((event) => event.type === "elicitation.completed")).toBe(true); + expect(events.some((event) => event.type === "human_response.recorded")).toBe( + false + ); + expect(response?.data.content).toBe("SDK_AUTOMATED_ASK_USER_DONE"); + } finally { + unsubscribe(); + await session.disconnect(); + } + } + ); + it( "session created with onElicitationRequest reports elicitation capability", { timeout: 60_000 }, diff --git a/nodejs/test/e2e/workflow.e2e.test.ts b/nodejs/test/e2e/workflow.e2e.test.ts index d5da188691..66718c35c2 100644 --- a/nodejs/test/e2e/workflow.e2e.test.ts +++ b/nodejs/test/e2e/workflow.e2e.test.ts @@ -23,10 +23,12 @@ const cliDistDirectory = process.env.COPILOT_EXTENSION_SDK_PATH ? dirname(process.env.COPILOT_EXTENSION_SDK_PATH) : dirname(cliPath); const workflowTestContext = await createSdkTestContext({ + logLevel: "debug", copilotClientOptions: { connection: RuntimeConnection.forStdio({ path: cliPath }), env: { - COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS,AGENT_FACTORIES", + RUST_LOG: "error,sdk_diagnostics=debug", + COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS", }, extensionLaunchProvider: { resolve: async (request) => ({ diff --git a/nodejs/test/external-tool-cancellation.test.ts b/nodejs/test/external-tool-cancellation.test.ts index 35d15bd8e3..d68e5f5bac 100644 --- a/nodejs/test/external-tool-cancellation.test.ts +++ b/nodejs/test/external-tool-cancellation.test.ts @@ -1,8 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + /* eslint-disable @typescript-eslint/no-explicit-any */ import { expect, it, vi } from "vitest"; +import { z } from "zod"; import { CopilotSession } from "../src/session.js"; +import { defineTool } from "../src/types.js"; import type { ToolInvocation } from "../src/types.js"; +it.each([ + ["custom", "*** Begin Patch\n*** End Patch"], + ["function", { input: "*** Begin Patch\n*** End Patch" }], +])( + "passes the patch string to a typed apply_patch override from a %s call", + async (_kind, args) => { + const sendRequest = vi.fn().mockResolvedValue(undefined); + const session = new CopilotSession("session-1", { sendRequest } as never); + const patchHandler = vi.fn((patch: string) => patch.trim()); + session.registerTools([ + defineTool("apply_patch", { + parameters: z.string(), + overridesBuiltInTool: true, + handler: patchHandler, + }), + ]); + + (session as any)._handleBroadcastEvent({ + type: "external_tool.requested", + data: { + requestId: "request-patch", + sessionId: "session-1", + toolCallId: "tool-call-patch", + toolName: "apply_patch", + arguments: args, + }, + }); + + await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledTimes(1)); + expect(patchHandler).toHaveBeenCalledWith( + "*** Begin Patch\n*** End Patch", + expect.objectContaining({ arguments: args }) + ); + expect(sendRequest).toHaveBeenCalledWith( + "session.tools.handlePendingToolCall", + expect.objectContaining({ result: "*** Begin Patch\n*** End Patch" }) + ); + } +); + +it.each([null, 42, {}, { input: 42 }])( + "reports invalid arguments for a string-schema apply_patch override: %j", + async (args) => { + const sendRequest = vi.fn().mockResolvedValue(undefined); + const session = new CopilotSession("session-1", { sendRequest } as never); + const handler = vi.fn((patch: string) => patch.trim()); + session.registerTools([ + defineTool("apply_patch", { + parameters: z.string(), + overridesBuiltInTool: true, + handler, + }), + ]); + + (session as any)._handleBroadcastEvent({ + type: "external_tool.requested", + data: { + requestId: "request-invalid-patch", + sessionId: "session-1", + toolCallId: "tool-call-invalid-patch", + toolName: "apply_patch", + arguments: args, + }, + }); + + await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledTimes(1)); + expect(handler).not.toHaveBeenCalled(); + expect(sendRequest).toHaveBeenCalledWith("session.tools.handlePendingToolCall", { + sessionId: "session-1", + requestId: "request-invalid-patch", + error: "apply_patch string override requires a string input", + }); + } +); + it("cancels a blocked external tool when completion arrives", async () => { const session = new CopilotSession("session-1", {} as never); let invocation: ToolInvocation | undefined; @@ -44,6 +125,34 @@ it("cancels a blocked external tool when completion arrives", async () => { expect(invocation?.signal?.aborted).toBe(true); }); +it("preserves object arguments for an object-schema apply_patch override", async () => { + const sendRequest = vi.fn().mockResolvedValue(undefined); + const session = new CopilotSession("session-1", { sendRequest } as never); + const handler = vi.fn(({ input }: { input: string }) => input); + session.registerTools([ + defineTool("apply_patch", { + parameters: z.object({ input: z.string() }), + overridesBuiltInTool: true, + handler, + }), + ]); + const argumentsObject = { input: "*** Begin Patch\n*** End Patch" }; + + (session as any)._handleBroadcastEvent({ + type: "external_tool.requested", + data: { + requestId: "request-object", + sessionId: "session-1", + toolCallId: "tool-call-object", + toolName: "apply_patch", + arguments: argumentsObject, + }, + }); + + await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledTimes(1)); + expect(handler).toHaveBeenCalledWith(argumentsObject, expect.anything()); +}); + it("does not respond when a cancelled handler returns a late result", async () => { const sendRequest = vi.fn().mockResolvedValue(undefined); const session = new CopilotSession("session-1", { sendRequest } as never); diff --git a/nodejs/test/ffiRuntimeHost.test.ts b/nodejs/test/ffiRuntimeHost.test.ts index e0fb3d71af..b5badfb27c 100644 --- a/nodejs/test/ffiRuntimeHost.test.ts +++ b/nodejs/test/ffiRuntimeHost.test.ts @@ -1,3 +1,7 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + /* eslint-disable @typescript-eslint/no-explicit-any */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; @@ -17,7 +21,14 @@ const ffi = vi.hoisted(() => { ) => callback(null, 11) ), }); - const hostShutdown = vi.fn(() => true); + const hostShutdown = Object.assign( + vi.fn(() => true), + { + async: vi.fn< + (serverId: number, callback: (error: Error | null, result: boolean) => void) => void + >(), + } + ); const connectionOpen = vi.fn(() => 21); const connectionWrite = vi.fn(() => true); const connectionClose = Object.assign(vi.fn<() => boolean>(), { @@ -82,6 +93,9 @@ describe("FfiRuntimeHost callback cleanup", () => { .mockImplementation((_id, callback) => callback(null, true)); ffi.connectionOpen.mockClear(); ffi.hostShutdown.mockClear(); + ffi.hostShutdown.async + .mockReset() + .mockImplementation((_id, callback) => callback(null, true)); ffi.hostStart.mockClear(); ffi.hostStart.async.mockClear(); ffi.register.mockClear(); @@ -90,6 +104,7 @@ describe("FfiRuntimeHost callback cleanup", () => { afterEach(() => { expect(ffi.connectionClose).not.toHaveBeenCalled(); + expect(ffi.hostShutdown).not.toHaveBeenCalled(); vi.clearAllTimers(); vi.useRealTimers(); }); @@ -103,7 +118,7 @@ describe("FfiRuntimeHost callback cleanup", () => { expect(ffi.connectionClose.async).toHaveBeenCalledTimes(1); expect(ffi.unregister).not.toHaveBeenCalled(); - expect(ffi.hostShutdown).not.toHaveBeenCalled(); + expect(ffi.hostShutdown.async).not.toHaveBeenCalled(); expect((host as any).outboundCallback).toBe(ffi.callbackToken); expect((host as any).keepAliveTimer).toBeDefined(); @@ -111,7 +126,7 @@ describe("FfiRuntimeHost callback cleanup", () => { expect(ffi.connectionClose.async).toHaveBeenCalledTimes(2); expect(ffi.unregister).toHaveBeenCalledTimes(1); - expect(ffi.hostShutdown).toHaveBeenCalledTimes(1); + expect(ffi.hostShutdown.async).toHaveBeenCalledTimes(1); expect((host as any).outboundCallback).toBeUndefined(); expect((host as any).keepAliveTimer).toBeUndefined(); expect(vi.getTimerCount()).toBe(0); @@ -120,7 +135,7 @@ describe("FfiRuntimeHost callback cleanup", () => { await vi.advanceTimersByTimeAsync(100); expect(ffi.connectionClose.async).toHaveBeenCalledTimes(2); expect(ffi.unregister).toHaveBeenCalledTimes(1); - expect(ffi.hostShutdown).toHaveBeenCalledTimes(1); + expect(ffi.hostShutdown.async).toHaveBeenCalledTimes(1); }); it("waits for successful initial cleanup without overlapping close calls", async () => { @@ -141,7 +156,7 @@ describe("FfiRuntimeHost callback cleanup", () => { expect(disposed).toBe(false); expect(ffi.connectionClose.async).toHaveBeenCalledTimes(1); expect(ffi.unregister).not.toHaveBeenCalled(); - expect(ffi.hostShutdown).not.toHaveBeenCalled(); + expect(ffi.hostShutdown.async).not.toHaveBeenCalled(); expect((host as any).outboundCallback).toBe(ffi.callbackToken); expect((host as any).keepAliveTimer).toBeDefined(); @@ -150,7 +165,7 @@ describe("FfiRuntimeHost callback cleanup", () => { expect(disposed).toBe(true); expect(ffi.unregister).toHaveBeenCalledTimes(1); - expect(ffi.hostShutdown).toHaveBeenCalledTimes(1); + expect(ffi.hostShutdown.async).toHaveBeenCalledTimes(1); expect(vi.getTimerCount()).toBe(0); }); @@ -172,7 +187,7 @@ describe("FfiRuntimeHost callback cleanup", () => { await vi.advanceTimersByTimeAsync(100); expect(ffi.unregister).toHaveBeenCalledTimes(1); - expect(ffi.hostShutdown).toHaveBeenCalledTimes(1); + expect(ffi.hostShutdown.async).toHaveBeenCalledTimes(1); expect(vi.getTimerCount()).toBe(0); }); @@ -195,7 +210,7 @@ describe("FfiRuntimeHost callback cleanup", () => { expect(ffi.connectionClose.async).toHaveBeenCalledTimes(1); expect(ffi.unregister).not.toHaveBeenCalled(); - expect(ffi.hostShutdown).not.toHaveBeenCalled(); + expect(ffi.hostShutdown.async).not.toHaveBeenCalled(); expect((host as any).outboundCallback).toBe(ffi.callbackToken); expect((FfiRuntimeHost as any).quarantinedHosts.has(host)).toBe(true); expect(error).toHaveBeenCalledTimes(1); @@ -205,7 +220,7 @@ describe("FfiRuntimeHost callback cleanup", () => { ); it("does not retry a terminal host shutdown failure", async () => { - ffi.hostShutdown.mockReturnValueOnce(false); + ffi.hostShutdown.async.mockImplementationOnce((_id, callback) => callback(null, false)); const error = vi.spyOn(console, "error").mockImplementation(() => {}); const host = FfiRuntimeHost.create("runtime.node", undefined, undefined, []); await host.start(); @@ -215,7 +230,7 @@ describe("FfiRuntimeHost callback cleanup", () => { expect(ffi.connectionClose.async).toHaveBeenCalledTimes(1); expect(ffi.unregister).toHaveBeenCalledTimes(1); - expect(ffi.hostShutdown).toHaveBeenCalledTimes(1); + expect(ffi.hostShutdown.async).toHaveBeenCalledTimes(1); expect(vi.getTimerCount()).toBe(0); error.mockRestore(); }); @@ -233,7 +248,7 @@ describe("FfiRuntimeHost callback cleanup", () => { expect(ffi.connectionClose.async).toHaveBeenCalledTimes(1); expect(ffi.unregister).toHaveBeenCalledTimes(1); - expect(ffi.hostShutdown).toHaveBeenCalledTimes(1); + expect(ffi.hostShutdown.async).toHaveBeenCalledTimes(1); expect((host as any).outboundCallback).toBe(ffi.callbackToken); expect((FfiRuntimeHost as any).quarantinedHosts.has(host)).toBe(true); expect((host as any).keepAliveTimer).toBeUndefined(); @@ -243,4 +258,149 @@ describe("FfiRuntimeHost callback cleanup", () => { expect(ffi.unregister).toHaveBeenCalledTimes(1); error.mockRestore(); }); + + it("joins host shutdown before startup rejects after disposal during startup", async () => { + let finishStart!: (error: Error | null, result: number) => void; + let finishShutdown: ((error: Error | null, result: boolean) => void) | undefined; + ffi.hostStart.async.mockImplementationOnce( + (_argv, _argvLength, _env, _envLength, callback) => { + finishStart = callback; + } + ); + ffi.hostShutdown.async.mockImplementationOnce((_id, callback) => { + finishShutdown = callback; + }); + const host = FfiRuntimeHost.create("runtime.node", undefined, undefined, []); + const settled = vi.fn(); + const startup = host.start().catch((error: unknown) => { + settled(); + return error; + }); + await host.dispose(); + finishStart(null, 11); + try { + await vi.advanceTimersByTimeAsync(0); + expect(ffi.hostShutdown.async).toHaveBeenCalledExactlyOnceWith( + 11, + expect.any(Function) + ); + expect(settled).not.toHaveBeenCalled(); + } finally { + finishShutdown?.(null, true); + await startup; + } + expect(await startup).toEqual( + new Error("The in-process runtime host was disposed during startup.") + ); + expect(host["serverId"]).toBe(0); + expect(ffi.connectionOpen).not.toHaveBeenCalled(); + }); + + it.each(["disposal", "connection-open failure"])( + "awaits asynchronous host shutdown while JS progresses during %s", + async (path) => { + let finishShutdown: ((error: Error | null, result: boolean) => void) | undefined; + ffi.hostShutdown.async.mockImplementationOnce((_id, callback) => { + finishShutdown = callback; + }); + const host = FfiRuntimeHost.create("runtime.node", undefined, undefined, []); + if (path === "connection-open failure") { + ffi.connectionOpen.mockReturnValueOnce(0); + } else { + await host.start(); + } + + const settled = vi.fn(); + const operation = (path === "disposal" ? host.dispose() : host.start()).then( + () => { + settled(); + return undefined; + }, + (error: unknown) => { + settled(); + return error; + } + ); + try { + const progress = vi.fn(); + setImmediate(progress); + await vi.advanceTimersByTimeAsync(0); + + expect(ffi.hostShutdown.async).toHaveBeenCalledExactlyOnceWith( + 11, + expect.any(Function) + ); + expect(progress).toHaveBeenCalledTimes(1); + expect(settled).not.toHaveBeenCalled(); + expect(host["serverId"]).toBe(11); + // Callback reclamation precedes host shutdown only once no live connection remains. + expect(ffi.connectionClose.async).toHaveBeenCalledTimes( + path === "disposal" ? 1 : 0 + ); + expect(ffi.unregister).toHaveBeenCalledExactlyOnceWith(ffi.callbackToken); + expect(host["outboundCallback"]).toBeUndefined(); + + await host.dispose(); + await vi.advanceTimersByTimeAsync(500); + + expect(settled).not.toHaveBeenCalled(); + expect(host["serverId"]).toBe(11); + expect(host["starting"]).toBe(path === "connection-open failure"); + expect(host["cleanupInProgress"]).toBe(path === "disposal"); + expect(ffi.hostShutdown.async).toHaveBeenCalledTimes(1); + expect(ffi.unregister).toHaveBeenCalledTimes(1); + } finally { + finishShutdown?.(null, true); + await operation; + await host.dispose(); + } + + if (path === "connection-open failure") { + expect(await operation).toEqual( + new Error("copilot_runtime_connection_open failed.") + ); + } else { + expect(await operation).toBeUndefined(); + } + expect(host["serverId"]).toBe(0); + expect(host["starting"]).toBe(false); + expect(host["cleanupInProgress"]).toBe(false); + expect(ffi.hostShutdown.async).toHaveBeenCalledTimes(1); + expect(ffi.unregister).toHaveBeenCalledTimes(1); + expect(vi.getTimerCount()).toBe(0); + } + ); + + it.each(["callback", "throw"])( + "preserves host shutdown %s errors during disposal", + async (failure) => { + const shutdownError = new Error("shutdown failed"); + ffi.hostShutdown.async.mockImplementationOnce((_id, callback) => { + if (failure === "throw") { + throw shutdownError; + } + callback(shutdownError, false); + }); + const error = vi.spyOn(console, "error").mockImplementation(() => {}); + const host = FfiRuntimeHost.create("runtime.node", undefined, undefined, []); + try { + await host.start(); + await host.dispose(); + await vi.advanceTimersByTimeAsync(500); + + expect(error).toHaveBeenCalledWith( + expect.stringContaining( + "Failed to shut down in-process FFI host: Error: shutdown failed" + ) + ); + expect(ffi.hostShutdown.async).toHaveBeenCalledTimes(1); + expect(ffi.unregister).toHaveBeenCalledTimes(1); + expect(host["serverId"]).toBe(0); + expect(vi.getTimerCount()).toBe(0); + } finally { + await host.dispose(); + error.mockRestore(); + } + } + ); }); diff --git a/nodejs/test/host-client.test.ts b/nodejs/test/host-client.test.ts index 22766bd81c..98d2f48e28 100644 --- a/nodejs/test/host-client.test.ts +++ b/nodejs/test/host-client.test.ts @@ -50,6 +50,36 @@ function info(hostId: string) { } describe("CopilotClient.startAhpHost", () => { + it("requires an explicit transport without starting the client", async () => { + const client = new CopilotClient(); + const start = vi.spyOn(client, "start"); + await expect(client.startAhpHost({})).rejects.toThrow("At least one"); + expect(start).not.toHaveBeenCalled(); + }); + + it.each([false, true])( + "forwards GitHub hosting with optional local transport (%s)", + async (local) => { + const githubEnvironment = { name: "Application", computeId: "stable-installation-id" }; + const start = vi.fn(({ hostId }: HostStartRequest) => ({ + hostId, + environmentId: "environment-1", + ...(local ? { url: "ws://127.0.0.1:54321", token: "local-token" } : {}), + })); + const client = await fixture((rpc) => rpc.onRequest("host.start", start)); + const options = { + githubEnvironment, + ...(local ? { localServer: {} } : {}), + }; + const host = await client.startAhpHost(options); + expect(start.mock.calls[0]?.[0]).toEqual({ hostId: host.hostId, ...options }); + expect(host.environmentId).toBe("environment-1"); + expect(host.url).toBe(local ? "ws://127.0.0.1:54321" : undefined); + expect(host.token).toBe(local ? "local-token" : undefined); + expect(host.pid).toBeUndefined(); + } + ); + it("snapshots factories and release callbacks at startup", async () => { let rpc!: MessageConnection; const client = await fixture((connection) => { @@ -65,6 +95,7 @@ describe("CopilotClient.startAhpHost", () => { const released = vi.fn(); const replacement = vi.fn(); const options: AhpHostOptions = { + localServer: {}, createSession: ({ config }) => client.createSession({ ...config, onPermissionRequest: approveAll }), resumeSession: ({ sessionId, config }) => @@ -109,6 +140,7 @@ describe("CopilotClient.startAhpHost", () => { expect(config).not.toHaveProperty("configDir"); }; const host = await client.startAhpHost({ + localServer: {}, createSession: ({ config }) => { checkConfig(config); return client.createSession({ ...config, onPermissionRequest: approveAll }); @@ -142,7 +174,7 @@ describe("CopilotClient.startAhpHost", () => { rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); }); const onExit = vi.fn(); - const host = await client.startAhpHost({ onExit }); + const host = await client.startAhpHost({ localServer: {}, onExit }); expect(host.pid).toBeUndefined(); const exit = { hostId: host.hostId, reason: "exited" }; await rpc.sendNotification("host.exited", exit); @@ -164,6 +196,7 @@ describe("CopilotClient.startAhpHost", () => { const released = vi.fn(); let original: Awaited>; const host = await client.startAhpHost({ + localServer: {}, createSession: async ({ config }) => { original = await client.createSession({ ...config, @@ -216,6 +249,7 @@ describe("CopilotClient.startAhpHost", () => { }); let original: Awaited> | undefined; const host = await client.startAhpHost({ + localServer: {}, createSession: async ({ config }) => { original = await client.createSession({ ...config, @@ -279,6 +313,7 @@ describe("CopilotClient.startAhpHost", () => { }); const released = vi.fn(); const host = await client.startAhpHost({ + localServer: {}, createSession: creates, resumeSession: ({ sessionId, config }) => client.resumeSession(sessionId, { ...config, onPermissionRequest: approveAll }), @@ -309,7 +344,7 @@ describe("CopilotClient.startAhpHost", () => { rpc = connection; rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); }); - const host = await client.startAhpHost({ createSession: creates }); + const host = await client.startAhpHost({ localServer: {}, createSession: creates }); await expect( rpc.sendRequest("host.materializeSession", { hostId: host.hostId, @@ -343,6 +378,7 @@ describe("CopilotClient.startAhpHost", () => { }); const released = vi.fn(); const host = await client.startAhpHost({ + localServer: {}, createSession: async (request) => { signal = request.signal; original = await client.createSession({ @@ -401,7 +437,7 @@ describe("CopilotClient.startAhpHost", () => { rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); rpc.onRequest("host.publishSession", publish); }); - const host = await client.startAhpHost(); + const host = await client.startAhpHost({ localServer: {} }); await expect(host.publishSession("existing")).resolves.toEqual({ sessionId: "existing", sessionUri: "copilot:/existing", @@ -424,6 +460,7 @@ describe("CopilotClient.startAhpHost", () => { }); const released = vi.fn(); const host = await client.startAhpHost({ + localServer: {}, createSession: ({ config }) => client.createSession({ ...config, @@ -458,6 +495,7 @@ describe("CopilotClient.startAhpHost", () => { }); const released = vi.fn(); const host = await client.startAhpHost({ + localServer: {}, createSession: async () => original, onSessionReleased: released, }); @@ -485,6 +523,7 @@ describe("CopilotClient.startAhpHost", () => { const onSessionReleased = vi.fn(); const toolHandler = vi.fn(() => "app-result"); const host = await client.startAhpHost({ + localServer: {}, createSession: ({ config }) => client.createSession({ ...config, @@ -541,6 +580,7 @@ describe("CopilotClient.startAhpHost", () => { let original: Awaited> | undefined; const released = vi.fn(); const host = await client.startAhpHost({ + localServer: {}, createSession: async (request) => { signal = request.signal; await pending; @@ -582,6 +622,7 @@ describe("CopilotClient.startAhpHost", () => { }); const released = vi.fn(); const host = await client.startAhpHost({ + localServer: {}, createSession: ({ config }) => client.createSession({ sessionId: config.sessionId, @@ -601,15 +642,15 @@ describe("CopilotClient.startAhpHost", () => { ); }); - it("supports omitted options and token-free responses without retaining callbacks", async () => { + it("supports explicit local defaults and token-free responses without retaining callbacks", async () => { const start = vi.fn(({ hostId }: HostStartRequest) => { const { token: _token, ...withoutToken } = info(hostId); return withoutToken; }); const client = await fixture((rpc) => rpc.onRequest("host.start", start)); - const host = await client.startAhpHost(); - expect(start.mock.calls[0]?.[0]).toEqual({ hostId: host.hostId }); + const host = await client.startAhpHost({ localServer: {} }); + expect(start.mock.calls[0]?.[0]).toEqual({ hostId: host.hostId, localServer: {} }); expect(host.token).toBeUndefined(); expect(client["hostExitCallbacks"].size).toBe(0); }); @@ -623,10 +664,10 @@ describe("CopilotClient.startAhpHost", () => { rpc.onRequest("host.dispose", dispose); }); - const host = await client.startAhpHost({ onExit }); + const host = await client.startAhpHost({ localServer: {}, onExit }); expect(start).toHaveBeenCalledOnce(); expect(host.hostId).toMatch(/^[a-f0-9-]{36}$/); - expect(start.mock.calls[0]?.[0]).toEqual({ hostId: host.hostId }); + expect(start.mock.calls[0]?.[0]).toEqual({ hostId: host.hostId, localServer: {} }); expect(client).not.toHaveProperty("startHost"); await Promise.all([host.dispose(), host.dispose()]); await host.dispose(); @@ -643,10 +684,12 @@ describe("CopilotClient.startAhpHost", () => { const client = await fixture((rpc) => rpc.onRequest("host.start", start)); const onExit = Object.assign(vi.fn(), { toJSON: () => "must-not-serialize" }); const options: AhpHostOptions & { hostId: string; workingDirectory: string } = { - hostname: "::1", - port: 0, - token: "explicit-test-token", - requireConnectionToken: false, + localServer: { + hostname: "::1", + port: 0, + token: "explicit-test-token", + requireConnectionToken: false, + }, onExit, hostId: "caller-cannot-select-id", workingDirectory: "/not-sent", @@ -655,10 +698,7 @@ describe("CopilotClient.startAhpHost", () => { const host = await client.startAhpHost(options); expect(start.mock.calls[0]?.[0]).toEqual({ hostId: host.hostId, - hostname: "::1", - port: 0, - token: "explicit-test-token", - requireConnectionToken: false, + localServer: options.localServer, }); expect(host.hostId).not.toBe(options.hostId); expect(onExit).not.toHaveBeenCalled(); @@ -670,10 +710,12 @@ describe("CopilotClient.startAhpHost", () => { }); const client = await fixture((rpc) => rpc.onRequest("host.start", start)); const options = { - hostname: "", - port: -1, - token: "", - requireConnectionToken: true, + localServer: { + hostname: "", + port: -1, + token: "", + requireConnectionToken: true, + }, }; await expect(client.startAhpHost(options)).rejects.toThrow( @@ -698,7 +740,7 @@ describe("CopilotClient.startAhpHost", () => { }); }); - const host = await client.startAhpHost({ onExit }); + const host = await client.startAhpHost({ localServer: {}, onExit }); expect(onExit).toHaveBeenCalledExactlyOnceWith({ hostId: host.hostId, reason: "exited", @@ -715,7 +757,9 @@ describe("CopilotClient.startAhpHost", () => { }); }); - await expect(client.startAhpHost({ onExit })).rejects.toThrow("address already in use"); + await expect(client.startAhpHost({ localServer: {}, onExit })).rejects.toThrow( + "address already in use" + ); expect(client["hostExitCallbacks"].size).toBe(0); await client.forceStop(); expect(onExit).not.toHaveBeenCalled(); @@ -730,7 +774,9 @@ describe("CopilotClient.startAhpHost", () => { }); }); - await expect(client.startAhpHost({ onExit })).rejects.toThrow("Startup failed after exit"); + await expect(client.startAhpHost({ localServer: {}, onExit })).rejects.toThrow( + "Startup failed after exit" + ); expect(onExit).toHaveBeenCalledOnce(); expect(client["hostExitCallbacks"].size).toBe(0); await client.forceStop(); @@ -746,7 +792,7 @@ describe("CopilotClient.startAhpHost", () => { }); }); - await expect(client.startAhpHost({ onExit })).rejects.toThrow(); + await expect(client.startAhpHost({ localServer: {}, onExit })).rejects.toThrow(); expect(onExit).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ reason: "ownerDisconnected" }) ); @@ -794,7 +840,7 @@ describe("CopilotClient.startAhpHost", () => { await client.start(); const unanswered = expect(client.ping()).rejects.toThrow(); const onExit = vi.fn(); - const host = await client.startAhpHost({ onExit }); + const host = await client.startAhpHost({ localServer: {}, onExit }); expect(onExit).toHaveBeenCalledExactlyOnceWith({ hostId: host.hostId, reason: "exited", @@ -814,6 +860,7 @@ describe("CopilotClient.startAhpHost", () => { let signal: AbortSignal | undefined; const onExit = vi.fn(); const host = await client.startAhpHost({ + localServer: {}, createSession: (request) => { signal = request.signal; return new Promise(() => {}); @@ -866,8 +913,8 @@ describe("CopilotClient.startAhpHost", () => { const firstExit = vi.fn(); const secondExit = vi.fn(); const [first, second] = await Promise.all([ - client.startAhpHost({ onExit: firstExit }), - client.startAhpHost({ onExit: secondExit }), + client.startAhpHost({ localServer: {}, onExit: firstExit }), + client.startAhpHost({ localServer: {}, onExit: secondExit }), ]); expect(first.hostId).not.toBe(second.hostId); @@ -889,7 +936,7 @@ describe("CopilotClient.startAhpHost", () => { rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); rpc.onRequest("host.dispose", dispose); }); - const host = await client.startAhpHost({ onExit }); + const host = await client.startAhpHost({ localServer: {}, onExit }); await serverRpc.sendNotification("host.exited", { hostId: "unrelated", reason: "exited" }); await client.ping(); expect(onExit).not.toHaveBeenCalled(); @@ -919,7 +966,7 @@ describe("CopilotClient.startAhpHost", () => { rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); rpc.onRequest("host.dispose", dispose); }); - const host = await client.startAhpHost({ onExit }); + const host = await client.startAhpHost({ localServer: {}, onExit }); await expect(host.dispose()).rejects.toThrow("Runtime cleanup failed"); expect(dispose).toHaveBeenCalledOnce(); expect(onExit).not.toHaveBeenCalled(); @@ -936,7 +983,7 @@ describe("CopilotClient.startAhpHost", () => { rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); rpc.onRequest("host.dispose", dispose); }); - await client.startAhpHost({ onExit }); + await client.startAhpHost({ localServer: {}, onExit }); await client[method](); expect(onExit).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ reason: "ownerDisconnected" }) @@ -963,7 +1010,7 @@ describe("CopilotClient.startAhpHost", () => { }); }); - const host = await client.startAhpHost({ onExit }); + const host = await client.startAhpHost({ localServer: {}, onExit }); expect(onExit).toHaveBeenCalledOnce(); expect(log).toHaveBeenCalledExactlyOnceWith("AHP host exit callback failed", { hostId: host.hostId, @@ -982,12 +1029,13 @@ describe("CopilotClient.startAhpHost", () => { rpc.onRequest("host.start", ({ hostId }: HostStartRequest) => info(hostId)); }); const first = await client.startAhpHost({ + localServer: {}, onExit: () => { throw error; }, }); const secondExit = vi.fn(); - await client.startAhpHost({ onExit: secondExit }); + await client.startAhpHost({ localServer: {}, onExit: secondExit }); await client.forceStop(); expect(log).toHaveBeenCalledExactlyOnceWith("AHP host exit callback failed", { hostId: first.hostId, @@ -1008,7 +1056,7 @@ describe("CopilotClient.startAhpHost", () => { disconnect = () => socket.destroy(); }); - const host = await client.startAhpHost({ onExit }); + const host = await client.startAhpHost({ localServer: {}, onExit }); disconnect?.(); await vi.waitFor(() => expect(onExit).toHaveBeenCalledExactlyOnceWith( diff --git a/nodejs/test/host.test.ts b/nodejs/test/host.test.ts index 3816112f41..36549de70b 100644 --- a/nodejs/test/host.test.ts +++ b/nodejs/test/host.test.ts @@ -4,6 +4,8 @@ import type { HostExitedNotification, HostStartRequest, HostStartResult, + HostLocalServerOptions, + HostGitHubEnvironmentOptions, } from "../src/generated/rpc.js"; const info = { @@ -16,10 +18,8 @@ describe("AhpHost", () => { it("keeps generated listener options optional but nonnullable", () => { expectTypeOf().toEqualTypeOf<{ hostId: string; - hostname?: string; - port?: number; - token?: string; - requireConnectionToken?: boolean; + localServer?: HostLocalServerOptions; + githubEnvironment?: HostGitHubEnvironmentOptions; sessionFactory?: boolean; resumeFactory?: boolean; }>(); @@ -27,6 +27,8 @@ describe("AhpHost", () => { expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf< number | null | undefined >(); @@ -50,6 +52,18 @@ describe("AhpHost", () => { expect(host.pid).toBe(1234); }); + it("supports GitHub-only hosts without a local URL, token, or process", () => { + const host = new AhpHost( + { hostId: "remote", environmentId: "environment-1" }, + vi.fn(), + vi.fn() + ); + expect(host.environmentId).toBe("environment-1"); + expect(host.url).toBeUndefined(); + expect(host.token).toBeUndefined(); + expect(host.pid).toBeUndefined(); + }); + it("supports a listener without a connection token", () => { const { token: _token, ...withoutToken } = info; const host = new AhpHost(withoutToken, vi.fn(), vi.fn()); diff --git a/nodejs/test/java-codegen.test.ts b/nodejs/test/java-codegen.test.ts new file mode 100644 index 0000000000..d44fb039a5 --- /dev/null +++ b/nodejs/test/java-codegen.test.ts @@ -0,0 +1,162 @@ +import { describe, expect, it } from "vitest"; + +import { renderRpcTypes, renderRpcWrappers } from "../../java/scripts/codegen/java.ts"; + +function rpcSource(files: Map, name: string): string { + const source = files.get( + `sdk/src/generated/java/com/github/copilot/generated/rpc/${name}.java` + ); + expect(source).toBeDefined(); + return source!; +} + +describe("java RPC codegen visibility", () => { + const schema = { + server: { + publicOnly: { + get: { + rpcMethod: "publicOnly.get", + params: { $ref: "#/definitions/PublicOnlyRequest" }, + result: { $ref: "#/definitions/PublicOnlyResult" }, + }, + }, + mixed: { + visible: { + rpcMethod: "mixed.visible", + params: null, + result: { $ref: "#/definitions/SharedResult" }, + }, + hidden: { + rpcMethod: "mixed.hidden", + visibility: "internal", + params: { $ref: "#/definitions/InternalOnlyRequest" }, + result: { $ref: "#/definitions/InternalOnlyResult" }, + }, + }, + internalOnly: { + hidden: { + rpcMethod: "internalOnly.hidden", + visibility: "internal", + params: { $ref: "#/definitions/InternalOnlyRequest" }, + result: { $ref: "#/definitions/InternalOnlyResult" }, + }, + }, + }, + definitions: { + PublicOnlyRequest: { + type: "object", + properties: { value: { type: "string" } }, + }, + PublicOnlyResult: { + type: "object", + properties: { shared: { $ref: "#/definitions/SharedResult" } }, + }, + SharedResult: { + type: "object", + properties: { ok: { type: "boolean" } }, + }, + InternalOnlyRequest: { + type: "object", + properties: { secret: { type: "string" } }, + }, + InternalOnlyResult: { + type: "object", + properties: { secret: { type: "string" } }, + }, + }, + } satisfies Parameters[0]; + + it("keeps internal-only RPC types package-private", async () => { + const files = await renderRpcTypes(schema, {}); + + expect(rpcSource(files, "PublicOnlyGetParams")).toContain( + "public record PublicOnlyGetParams(" + ); + expect(rpcSource(files, "SharedResult")).toContain("public record SharedResult("); + expect(rpcSource(files, "MixedHiddenParams")).toContain("record MixedHiddenParams("); + expect(rpcSource(files, "MixedHiddenParams")).not.toContain( + "public record MixedHiddenParams(" + ); + expect(rpcSource(files, "InternalOnlyResult")).toContain("record InternalOnlyResult("); + expect(rpcSource(files, "InternalOnlyResult")).not.toContain( + "public record InternalOnlyResult(" + ); + }); + + it("does not expose internal methods or namespaces through public Java members", async () => { + const files = await renderRpcWrappers(schema); + + expect(rpcSource(files, "ServerRpc")).toContain( + "public final ServerPublicOnlyApi publicOnly;" + ); + expect(rpcSource(files, "ServerRpc")).toContain("public final ServerMixedApi mixed;"); + expect(rpcSource(files, "ServerRpc")).toContain( + "final ServerInternalOnlyApi internalOnly;" + ); + expect(rpcSource(files, "ServerRpc")).not.toContain( + "public final ServerInternalOnlyApi internalOnly;" + ); + + expect(rpcSource(files, "ServerMixedApi")).toContain("public final class ServerMixedApi"); + expect(rpcSource(files, "ServerMixedApi")).toContain( + "public CompletableFuture visible()" + ); + expect(rpcSource(files, "ServerMixedApi")).toContain( + "CompletableFuture hidden(MixedHiddenParams params)" + ); + expect(rpcSource(files, "ServerMixedApi")).not.toContain( + "public CompletableFuture hidden" + ); + + expect(rpcSource(files, "ServerInternalOnlyApi")).toContain( + "final class ServerInternalOnlyApi" + ); + expect(rpcSource(files, "ServerInternalOnlyApi")).not.toContain( + "public final class ServerInternalOnlyApi" + ); + }); + + it("reuses a shared result definition for internal methods", async () => { + const sharedResultSchema = { + server: { + globalState: { + load: { + rpcMethod: "globalState.load", + visibility: "internal", + params: null, + result: { $ref: "#/definitions/GlobalStateLoadResult" }, + }, + loadForConfigDir: { + rpcMethod: "globalState.loadForConfigDir", + visibility: "internal", + params: null, + result: { $ref: "#/definitions/GlobalStateLoadResult" }, + }, + }, + }, + definitions: { + GlobalStateLoadResult: { + type: "object", + properties: { staff: { type: "boolean" } }, + }, + }, + } satisfies Parameters[0]; + const types = await renderRpcTypes(sharedResultSchema, {}); + const wrappers = await renderRpcWrappers(sharedResultSchema); + + expect(rpcSource(wrappers, "ServerGlobalStateApi")).toContain( + "CompletableFuture loadForConfigDir()" + ); + expect(rpcSource(wrappers, "ServerGlobalStateApi")).toContain( + "CompletableFuture load()" + ); + expect(rpcSource(types, "GlobalStateLoadResult")).not.toContain( + "public record GlobalStateLoadResult(" + ); + expect( + types.has( + "sdk/src/generated/java/com/github/copilot/generated/rpc/GlobalStateLoadForConfigDirResult.java" + ) + ).toBe(false); + }); +}); diff --git a/nodejs/test/mcp-list.test.ts b/nodejs/test/mcp-list.test.ts new file mode 100644 index 0000000000..8b19e799eb --- /dev/null +++ b/nodejs/test/mcp-list.test.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { PassThrough } from "node:stream"; +import { createMessageConnection } from "vscode-jsonrpc/node.js"; +import { describe, expect, it, vi } from "vitest"; +import { createSessionRpc } from "../src/generated/rpc.js"; + +describe("session MCP wire contracts", () => { + it("uses parameterless list endpoints with the wrapper session", async () => { + const input = new PassThrough(); + const output = new PassThrough(); + const connection = createMessageConnection(input, output); + const sendRequest = vi.spyOn(connection, "sendRequest").mockResolvedValue({ servers: [] }); + try { + const mcp = createSessionRpc(connection, "wrapper-session").mcp; + await mcp.list(); + await mcp.listConfigured(); + expect(sendRequest.mock.calls).toEqual([ + ["session.mcp.list", { sessionId: "wrapper-session" }], + ["session.mcp.listConfigured", { sessionId: "wrapper-session" }], + ]); + } finally { + connection.dispose(); + input.destroy(); + output.destroy(); + } + }); + + it("keeps the wrapper session authoritative over a supplied sessionId", async () => { + const input = new PassThrough(); + const output = new PassThrough(); + const connection = createMessageConnection(input, output); + const sendRequest = vi.spyOn(connection, "sendRequest").mockResolvedValue(undefined); + try { + const mcp = createSessionRpc(connection, "wrapper-session").mcp; + const params = { serverName: "test-server", sessionId: "forged-session" }; + await mcp.enable(params); + expect(sendRequest.mock.calls).toEqual([ + ["session.mcp.enable", { serverName: "test-server", sessionId: "wrapper-session" }], + ]); + } finally { + connection.dispose(); + input.destroy(); + output.destroy(); + } + }); +}); diff --git a/nodejs/test/mcp-prompts.test.ts b/nodejs/test/mcp-prompts.test.ts new file mode 100644 index 0000000000..8272fd4062 --- /dev/null +++ b/nodejs/test/mcp-prompts.test.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { readFileSync } from "node:fs"; +import { describe, expect, it, vi } from "vitest"; +import type { MessageConnection } from "vscode-jsonrpc/node.js"; +import { createSessionRpc } from "../src/generated/rpc.js"; + +const fixtures = JSON.parse( + readFileSync(new URL("../../test/harness/mcp-prompt-fixtures.json", import.meta.url), "utf8") +); + +// These dispatch checks supplement the real MCP transport tests in rpc_mcp_and_skills. +describe("generated MCP prompt RPCs", () => { + it("keeps the bound session when prompt and existing RPC params contain another session ID", async () => { + const sendRequest = vi.fn().mockResolvedValue({}); + const rpc = createSessionRpc( + { sendRequest } as unknown as MessageConnection, + "bound-session" + ); + const listParams = { + sessionId: "foreign-session", + serverName: "fixture", + cursor: "opaque-cursor", + }; + const getParams = { + sessionId: "foreign-session", + serverName: "fixture", + promptName: "rich", + arguments: { topic: "preserved" }, + }; + const sendParams = { sessionId: "foreign-session", prompt: "preserved" }; + + await rpc.mcp.prompts.list(listParams); + await rpc.mcp.prompts.get(getParams); + await rpc.send(sendParams); + + expect(sendRequest.mock.calls).toEqual([ + ["session.mcp.prompts.list", { ...listParams, sessionId: "bound-session" }], + ["session.mcp.prompts.get", { ...getParams, sessionId: "bound-session" }], + ["session.send", { ...sendParams, sessionId: "bound-session" }], + ]); + }); + + it("binds the session and forwards the opaque list cursor", async () => { + const sendRequest = vi + .fn() + .mockResolvedValueOnce(fixtures.firstPage) + .mockResolvedValueOnce(fixtures.secondPage); + const rpc = createSessionRpc( + { sendRequest } as unknown as MessageConnection, + "bound-session" + ); + + const first = await rpc.mcp.prompts.list({ serverName: "fixture" }); + expect(first).toEqual(fixtures.firstPage); + expect(sendRequest).toHaveBeenNthCalledWith(1, "session.mcp.prompts.list", { + sessionId: "bound-session", + serverName: "fixture", + }); + const second = await rpc.mcp.prompts.list({ + serverName: "fixture", + cursor: first.nextCursor, + }); + expect(second).toEqual(fixtures.secondPage); + expect(sendRequest).toHaveBeenNthCalledWith(2, "session.mcp.prompts.list", { + sessionId: "bound-session", + serverName: "fixture", + cursor: first.nextCursor, + }); + }); + + it.each | undefined>([undefined, {}, { topic: "日本語", style: "" }])( + "preserves optional arguments %j and opaque results", + async (args) => { + const sendRequest = vi.fn().mockResolvedValue(fixtures.richPrompt); + const rpc = createSessionRpc( + { sendRequest } as unknown as MessageConnection, + "bound-session" + ); + const params = { + serverName: "fixture", + promptName: "rich", + ...(args === undefined ? {} : { arguments: args }), + }; + expect(await rpc.mcp.prompts.get(params)).toEqual(fixtures.richPrompt); + expect(sendRequest).toHaveBeenCalledExactlyOnceWith("session.mcp.prompts.get", { + ...params, + sessionId: "bound-session", + }); + } + ); + + it("does not swallow upstream errors", async () => { + const error = new Error("Missing required argument: topic"); + const sendRequest = vi.fn().mockRejectedValue(error); + const rpc = createSessionRpc( + { sendRequest } as unknown as MessageConnection, + "bound-session" + ); + await expect( + rpc.mcp.prompts.get({ serverName: "fixture", promptName: "rich" }) + ).rejects.toBe(error); + }); +}); diff --git a/nodejs/test/npm-release.test.ts b/nodejs/test/npm-release.test.ts index f3d26da5df..04bed8b8d9 100644 --- a/nodejs/test/npm-release.test.ts +++ b/nodejs/test/npm-release.test.ts @@ -1,8 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + import { createHash } from "node:crypto"; -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { dirname, join, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { join, resolve } from "node:path"; import { describe, expect, it, vi } from "vitest"; import { assertVersionAbsent, publishManifest, publishTarball } from "../scripts/npm-release.js"; @@ -166,32 +169,3 @@ describe("npm release publishing", () => { } }); }); - -const workflow = readFileSync( - resolve(dirname(fileURLToPath(import.meta.url)), "../../.github/workflows/publish.yml"), - "utf8" -).replaceAll("\r\n", "\n"); - -function workflowJob(jobId: string): string { - const marker = ` ${jobId}:\n`; - const start = workflow.indexOf(marker); - if (start < 0) throw new Error(`Workflow job not found: ${jobId}`); - const rest = workflow.slice(start + marker.length); - const nextJob = rest.search(/^ [a-z0-9-]+:\n/m); - return nextJob < 0 ? rest : rest.slice(0, nextJob); -} - -describe("runtime-backed npm publishing workflow", () => { - it.each(["runtime-publish-internal", "runtime-publish-public"])( - "%s validates retained packages before using the shared publisher", - (jobId) => { - const job = workflowJob(jobId); - const validation = job.indexOf("- name: Validate retained release"); - const publication = job.indexOf("npm-release.js publish-manifest"); - - expect(validation).toBeGreaterThanOrEqual(0); - expect(publication).toBeGreaterThan(validation); - expect(job.match(/npm-release\.js publish-manifest/g)).toHaveLength(1); - } - ); -}); diff --git a/nodejs/test/package-sdk.test.ts b/nodejs/test/package-sdk.test.ts index aa521e26fd..cabbe09300 100644 --- a/nodejs/test/package-sdk.test.ts +++ b/nodejs/test/package-sdk.test.ts @@ -46,6 +46,7 @@ describe("release packaging", () => { "src/runtimeArtifacts.ts", "src/cliVersion.ts", "../scripts/runtime-layout.mjs", + "../scripts/runtime-release.mjs", ]) { const destination = join(nodeRoot, file); mkdirSync(dirname(destination), { recursive: true }); @@ -56,13 +57,16 @@ describe("release packaging", () => { join(nodeRoot, "node_modules"), "junction" ); + const { files } = JSON.parse( + readFileSync(join(sourceNodeRoot, "package.json"), "utf8") + ) as { files: string[] }; const manifest = JSON.stringify({ name: "@github/copilot-sdk", version: "0.0.0-packaging-test", type: "module", repository: "https://github.com/github/copilot-sdk.git", dependencies: {}, - files: ["dist"], + files, scripts: { "pack:release": "tsx scripts/package-sdk.ts", "verify:release-packages": "tsx scripts/verify-release-packages.ts", @@ -71,6 +75,8 @@ describe("release packaging", () => { write(join(nodeRoot, "package.json"), manifest); write(join(nodeRoot, "dist/index.js"), "export {};"); write(join(nodeRoot, "dist/cjs/index.js"), "module.exports = {};"); + write(join(nodeRoot, "dist/index.d.ts"), "export {};"); + write(join(nodeRoot, "dist/tsconfig.tsbuildinfo"), "local build metadata"); const environment = { ...process.env }; for (const key of [ @@ -161,6 +167,15 @@ globalThis.fetch = async (url) => { expect( readFileSync(join(unpacked, "package/prebuilds/linux-x64/runtime.node"), "utf8") ).toBe(`${source} runtime: linux-x64`); + const sdkUnpacked = join(root, "unpacked-sdk"); + mkdirSync(sdkUnpacked); + await extractTar({ + file: join(nodeRoot, "github-copilot-sdk-0.0.0-packaging-test.tgz"), + cwd: sdkUnpacked, + }); + expect(existsSync(join(sdkUnpacked, "package/dist/index.js"))).toBe(true); + expect(existsSync(join(sdkUnpacked, "package/dist/index.d.ts"))).toBe(true); + expect(existsSync(join(sdkUnpacked, "package/dist/tsconfig.tsbuildinfo"))).toBe(false); rmSync(join(packageRoots[0], "copilot-sdk/extension.js")); const incompletePack = run("pack:release"); diff --git a/nodejs/test/python-codegen.test.ts b/nodejs/test/python-codegen.test.ts index bbad9e216d..f189e9619d 100644 --- a/nodejs/test/python-codegen.test.ts +++ b/nodejs/test/python-codegen.test.ts @@ -2,12 +2,41 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { + appendLastPythonRpcConstructorFields, applyPythonLegacyParameters, emitMethod, generatePythonSessionEventsCode, + isPythonObjectResultSchema, + pythonAppendLastFieldsPresentIn, } from "../../scripts/codegen/python.ts"; import { legacyRequestSchema } from "./legacy-parameters-fixture.ts"; +describe("Python referenced RPC results", () => { + it("uses record deserialization for nullable object references, but not enums", () => { + const definitions = { + definitions: { + Account: { + type: "object" as const, + properties: { accountId: { type: "string" as const } }, + required: ["accountId"], + }, + AccountResult: { + anyOf: [{ $ref: "#/definitions/Account" }, { type: "null" as const }], + }, + Mode: { type: "string" as const, enum: ["token"] }, + }, + }; + + expect(isPythonObjectResultSchema({ $ref: "#/definitions/Account" }, definitions)).toBe( + true + ); + expect( + isPythonObjectResultSchema({ $ref: "#/definitions/AccountResult" }, definitions) + ).toBe(true); + expect(isPythonObjectResultSchema({ $ref: "#/definitions/Mode" }, definitions)).toBe(false); + }); +}); + describe("Python RPC projection compatibility", () => { const code = readFileSync( new URL("../../python/copilot/generated/rpc.py", import.meta.url), @@ -181,3 +210,91 @@ describe("Python x-legacy-parameters", () => { ).toBe(snippet); }); }); + +describe("Python append-last RPC fields", () => { + const fields = [["Request", "addedField"]] as const; + const request = (docstring: string) => + [ + "@dataclass", + "class Request:", + ` """${docstring}"""`, + "", + " added_field: str | None = None", + " stable_field: str | None = None", + "", + " @staticmethod", + " def from_dict(obj: Any) -> 'Request':", + ' added_field = from_str(obj.get("addedField"))', + ' stable_field = from_str(obj.get("stableField"))', + " return Request(added_field, stable_field)", + "", + ].join("\n"); + + it("reorders a dataclass that is the last block in the file", () => { + const updated = appendLastPythonRpcConstructorFields(request("Request."), fields); + + expect(updated.indexOf("stable_field: str")).toBeLessThan( + updated.indexOf("added_field: str") + ); + expect(updated).toContain("return Request(stable_field, added_field)"); + }); + + it("does not end the dataclass block at a capital Z before its constructor", () => { + const updated = appendLastPythonRpcConstructorFields( + `${request("Zone request.")}\n@dataclass\nclass Next:\n pass\n`, + fields + ); + + expect(updated.indexOf("stable_field: str")).toBeLessThan( + updated.indexOf("added_field: str") + ); + expect(updated).toContain("return Request(stable_field, added_field)"); + expect(updated).toContain("class Next:"); + }); + + it("fails generation instead of skipping a missing dataclass", () => { + expect(() => + appendLastPythonRpcConstructorFields("@dataclass\nclass Other:\n pass\n", fields) + ).toThrow("Missing dataclass Request"); + }); +}); + +describe("Python append-last fields for a selected schema", () => { + const fields = [["Request", "addedField"]] as const; + + it("keeps an entry whose property the schema declares", () => { + expect( + pythonAppendLastFieldsPresentIn( + { Request: { type: "object", properties: { addedField: { type: "string" } } } }, + fields + ) + ).toEqual(fields); + }); + + it("includes fields marked append-last by the contract schema", () => { + expect( + pythonAppendLastFieldsPresentIn({ + Response: { + type: "object", + properties: { + stableField: { type: "string" }, + addedField: { + type: "string", + "x-copilot-sdk-append-last": true, + }, + }, + }, + }) + ).toContainEqual(["Response", "addedField"]); + }); + + it("skips an entry for a legacy request or a schema without the definition", () => { + expect( + pythonAppendLastFieldsPresentIn( + { Request: { type: "object", properties: { stableField: { type: "string" } } } }, + fields + ) + ).toEqual([]); + expect(pythonAppendLastFieldsPresentIn({}, fields)).toEqual([]); + }); +}); diff --git a/nodejs/test/release-manifest.test.ts b/nodejs/test/release-manifest.test.ts index 5b389a11e9..cc4fe8d508 100644 --- a/nodejs/test/release-manifest.test.ts +++ b/nodejs/test/release-manifest.test.ts @@ -1,19 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; import { join } from "node:path"; import { c as createTar } from "tar"; import { afterEach, describe, expect, it } from "vitest"; -import { - createPackageSetManifest, - createReleaseManifest, - verifyPackageSetManifest, - verifyReleaseManifest, -} from "../scripts/release-manifest.js"; +import { createPackageSetManifest, verifyPackageSetManifest } from "../scripts/release-manifest.js"; import { getRuntimePackageName, RUNTIME_PLATFORMS } from "../src/runtimeArtifacts.js"; const roots: string[] = []; -const sdkSha = "abcdef0123456789abcdef0123456789abcdef01"; -const runtimeSha = "123456789abcdef0123456789abcdef012345678"; afterEach(() => { for (const root of roots.splice(0)) { @@ -31,7 +27,7 @@ async function packageTarball(root: string, name: string, version: string): Prom describe("release manifest", () => { it("freezes and verifies a direct nine-package release", async () => { - const root = mkdtempSync(join(tmpdir(), "copilot-sdk-package-set-manifest-")); + const root = mkdtempSync(join(process.cwd(), ".sdk-package-set-manifest-")); roots.push(root); const version = "1.0.13-unstable.34640000001.gabcdef0"; for (const name of [ @@ -56,75 +52,4 @@ describe("release manifest", () => { writeFileSync(damaged, Buffer.concat([readFileSync(damaged), Buffer.from("tampered")])); expect(() => verifyPackageSetManifest(manifest, root)).toThrow("Size mismatch"); }); - - it("freezes and verifies the exact nine-package release identity", async () => { - const root = mkdtempSync(join(tmpdir(), "copilot-sdk-manifest-")); - roots.push(root); - const version = "1.0.13-unstable.34640000001.gabcdef0"; - for (const name of [ - "@github/copilot-sdk", - ...RUNTIME_PLATFORMS.map(getRuntimePackageName), - ]) { - await packageTarball(root, name, version); - } - const manifest = await createReleaseManifest(root, { - channel: "unstable", - createdAt: "2026-09-04T00:00:00Z", - runtimeRunId: "9001", - runtimeSha, - runtimeVersion: "1.0.83-5.unstable.r9001.g1234567", - sdkRef: "feature/unstable", - sdkSha, - sdkVersion: version, - testPolicy: "advisory", - workflowRunId: "812300", - workflowRunNumber: "8123", - }); - - expect(manifest.packages).toHaveLength(9); - expect(manifest.runtime.runId).toBe("9001"); - expect(manifest.runtime.source).toBe("github-packages"); - expect(manifest.workflow.testPolicy).toBe("advisory"); - expect(() => verifyReleaseManifest(manifest, root)).not.toThrow(); - - const mismatched = structuredClone(manifest); - mismatched.runtime.version = "1.0.83-5.canary.r9001.g1234567.unsigned"; - expect(() => verifyReleaseManifest(mismatched, root)).toThrow("channel identifier"); - const mismatchedRun = structuredClone(manifest); - mismatchedRun.runtime.runId = "9002"; - expect(() => verifyReleaseManifest(mismatchedRun, root)).toThrow("run_id"); - const mismatchedSha = structuredClone(manifest); - mismatchedSha.runtime.sha = "abcdef0123456789abcdef0123456789abcdef01"; - expect(() => verifyReleaseManifest(mismatchedSha, root)).toThrow("runtime sha"); - const buildMetadata = structuredClone(manifest); - buildMetadata.runtime.version += "+build.42"; - expect(() => verifyReleaseManifest(buildMetadata, root)).toThrow( - "must not contain build metadata" - ); - await expect( - createReleaseManifest(root, { - channel: "canary", - createdAt: "2026-09-04T00:00:00Z", - runtimeRunId: "9001", - runtimeSha, - runtimeVersion: "1.0.83-5.unstable.r9001.g1234567", - sdkRef: "feature/unstable", - sdkSha, - sdkVersion: version, - testPolicy: "required", - workflowRunId: "812300", - workflowRunNumber: "8123", - }) - ).rejects.toThrow("channel identifier"); - - const invalidPolicy = structuredClone(manifest); - invalidPolicy.workflow.testPolicy = "optional" as "required"; - expect(() => verifyReleaseManifest(invalidPolicy, root)).toThrow( - "Invalid runtime E2E test policy" - ); - - const damaged = join(root, manifest.packages[0].filename); - writeFileSync(damaged, Buffer.concat([readFileSync(damaged), Buffer.from("tampered")])); - expect(() => verifyReleaseManifest(manifest, root)).toThrow("Size mismatch"); - }); }); diff --git a/nodejs/test/runtime-package-acquisition.test.ts b/nodejs/test/runtime-package-acquisition.test.ts deleted file mode 100644 index ae36d334d6..0000000000 --- a/nodejs/test/runtime-package-acquisition.test.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { createHash } from "node:crypto"; -import { spawnSync } from "node:child_process"; -import { - chmodSync, - copyFileSync, - existsSync, - mkdirSync, - mkdtempSync, - readFileSync, - rmSync, - statSync, - writeFileSync, -} from "node:fs"; -import { tmpdir } from "node:os"; -import { basename, join } from "node:path"; -import { c as createTar } from "tar"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { - acquireRuntimePackages, - getSourceRuntimePackageName, - parseArguments, - validateRuntimePackageRoot, -} from "../scripts/runtime-package-acquisition.js"; -import { RUNTIME_PLATFORMS } from "../src/runtimeArtifacts.js"; - -const roots: string[] = []; -const runtimeVersion = "1.0.83-5.unstable.123.gabcdef0"; -const runtimeSha = "abcdef0123456789abcdef0123456789abcdef01"; - -function temporaryRoot(prefix: string): string { - const root = mkdtempSync(join(tmpdir(), prefix)); - roots.push(root); - return root; -} - -afterEach(() => { - for (const root of roots.splice(0)) { - rmSync(root, { recursive: true, force: true }); - } -}); - -async function createRuntimePackage(root: string, platform: string): Promise { - const packageRoot = join(root, platform, "package"); - const windows = platform.startsWith("win32"); - const [osName, cpu] = platform.replace("linuxmusl", "linux").split("-"); - mkdirSync(join(packageRoot, "prebuilds", platform), { recursive: true }); - mkdirSync(join(packageRoot, "copilot-sdk"), { recursive: true }); - mkdirSync(join(packageRoot, "preloads"), { recursive: true }); - writeFileSync( - join(packageRoot, "package.json"), - JSON.stringify({ - name: getSourceRuntimePackageName(platform), - version: runtimeVersion, - repository: "https://github.com/github/copilot-agent-runtime.git", - os: [osName], - cpu: [cpu], - ...(platform.startsWith("linux") - ? { libc: [platform.startsWith("linuxmusl") ? "musl" : "glibc"] } - : {}), - copilotRuntime: { - sourceRepository: "github/copilot-agent-runtime", - sourceSha: runtimeSha, - }, - }) - ); - const executablePaths = new Set([ - windows ? "copilot.exe" : "copilot", - join("prebuilds", platform, windows ? "copilot-runtime.exe" : "copilot-runtime"), - ]); - for (const path of [ - "LICENSE.md", - ...executablePaths, - join("prebuilds", platform, "runtime.node"), - join("copilot-sdk", "extension.js"), - join("preloads", "extension_bootstrap.mjs"), - ]) { - const destination = join(packageRoot, path); - writeFileSync(destination, path); - if (executablePaths.has(path)) { - chmodSync(destination, 0o755); - } - } - const archive = join(root, `${platform}.tgz`); - await createTar({ cwd: join(root, platform), file: archive, gzip: true }, ["package"]); - return archive; -} - -describe("runtime npm package acquisition", () => { - it("requires exactly one non-empty value for every CLI option", () => { - const valid = [ - "--version", - runtimeVersion, - "--sha", - runtimeSha, - "--output", - "runtime-packages", - ]; - expect(parseArguments(valid)).toEqual({ - outputDirectory: "runtime-packages", - runtimeSha, - runtimeVersion, - }); - for (const invalid of [ - valid.slice(0, -2), - [...valid.slice(0, -2), "--outpt", "runtime-packages"], - [...valid.slice(0, -2), "--sha", runtimeSha], - [...valid.slice(0, -1), ""], - [...valid.slice(0, -1), "--unknown"], - ]) { - expect(() => parseArguments(invalid)).toThrow(); - } - }); - - it("downloads all platforms and creates a compact executable-preserving handoff", async () => { - const root = temporaryRoot("copilot-runtime-acquisition-"); - const output = join(root, "runtime-packages"); - const archives = new Map(); - for (const platform of RUNTIME_PLATFORMS) { - const path = await createRuntimePackage(root, platform); - archives.set(platform, { - path, - integrity: `sha512-${createHash("sha512") - .update(readFileSync(path)) - .digest("base64")}`, - }); - } - const runner = vi.fn(async (_command: string, args: string[]) => { - const spec = args[1]; - const platform = RUNTIME_PLATFORMS.find((candidate) => - spec.startsWith(`${getSourceRuntimePackageName(candidate)}@`) - ); - expect(platform).toBeDefined(); - const archive = archives.get(platform!)!; - if (args[0] === "view") { - return { status: 0, stdout: JSON.stringify(archive.integrity), stderr: "" }; - } - const destination = args[args.indexOf("--pack-destination") + 1]; - const filename = basename(archive.path); - mkdirSync(destination, { recursive: true }); - copyFileSync(archive.path, join(destination, filename)); - return { - status: 0, - stdout: JSON.stringify([{ filename, integrity: archive.integrity }]), - stderr: "", - }; - }); - - await acquireRuntimePackages( - { - outputDirectory: output, - runtimeSha, - runtimeVersion, - }, - runner - ); - - expect(runner).toHaveBeenCalledTimes(16); - const acquisition = JSON.parse(readFileSync(join(output, "runtime-packages.json"), "utf8")); - expect(acquisition.packages).toHaveLength(8); - for (const platform of RUNTIME_PLATFORMS) { - validateRuntimePackageRoot( - join(output, platform), - platform, - runtimeVersion, - runtimeSha - ); - } - - const archive = join(root, "runtime-packages.tar.gz"); - const archived = spawnSync( - "tar", - [ - "-czf", - basename(archive), - "--exclude", - "runtime-packages/tarballs", - "runtime-packages", - ], - { cwd: root, encoding: "utf8" } - ); - expect(archived.status, archived.stderr).toBe(0); - - const extractedRoot = join(root, "extracted"); - mkdirSync(extractedRoot); - const extracted = spawnSync("tar", ["-xzf", basename(archive), "-C", "extracted"], { - cwd: root, - encoding: "utf8", - }); - expect(extracted.status, extracted.stderr).toBe(0); - const handoffRoot = join(extractedRoot, "runtime-packages"); - expect(existsSync(join(handoffRoot, "runtime-packages.json"))).toBe(true); - expect(existsSync(join(handoffRoot, "tarballs"))).toBe(false); - for (const platform of RUNTIME_PLATFORMS) { - validateRuntimePackageRoot( - join(handoffRoot, platform), - platform, - runtimeVersion, - runtimeSha - ); - } - if (process.platform !== "win32") { - expect(statSync(join(handoffRoot, "linux-x64", "copilot")).mode & 0o111).not.toBe(0); - } - }); - - it("requires strict GitHub Packages registry integrity", async () => { - const root = temporaryRoot("copilot-runtime-registry-"); - const runner = vi - .fn() - .mockResolvedValue({ status: 0, stdout: JSON.stringify("sha1-invalid"), stderr: "" }); - await expect( - acquireRuntimePackages( - { - outputDirectory: join(root, "output"), - runtimeSha, - runtimeVersion, - }, - runner - ) - ).rejects.toThrow("Invalid registry integrity"); - expect(runner.mock.calls[0][1]).toContain("https://npm.pkg.github.com"); - }); - - it("rejects mismatched source identity metadata", async () => { - const root = temporaryRoot("copilot-runtime-identity-"); - await createRuntimePackage(root, "linux-x64"); - const packageRoot = join(root, "linux-x64", "package"); - const manifestPath = join(packageRoot, "package.json"); - const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); - manifest.copilotRuntime.sourceSha = "0".repeat(40); - writeFileSync(manifestPath, JSON.stringify(manifest)); - - expect(() => - validateRuntimePackageRoot(packageRoot, "linux-x64", runtimeVersion, runtimeSha) - ).toThrow(); - }); -}); diff --git a/nodejs/test/runtime-release-identity.test.ts b/nodejs/test/runtime-release-identity.test.ts deleted file mode 100644 index e1184bbc31..0000000000 --- a/nodejs/test/runtime-release-identity.test.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - type ReleaseDispatchInputs, - validateReleaseDispatch, - validateRuntimeReleaseIdentity, -} from "../scripts/runtime-release-identity.js"; - -const runtime = { - run_id: "34640000001", - sha: "abcdef0123456789abcdef0123456789abcdef01", - version: "1.0.83-5.unstable.r34640000001.gabcdef0", -}; -const inputs: ReleaseDispatchInputs = { - distTag: "unstable", - mode: "publish", - runtimeJson: "", - testPolicy: "required", - version: "", -}; - -describe("runtime version identity", () => { - it.each([ - ["canary", "1.2.4-canary.r34640000001.gabcdef0.signed"], - ["canary", "1.2.4-canary.r34640000001.gabcdef0.unsigned"], - ["canary", "1.2.4-7.canary.r34640000001.gabcdef0.signed"], - ["canary", "1.2.4-7.canary.r34640000001.gabcdef0.unsigned"], - ["unstable", "1.2.4-unstable.r34640000001.gabcdef0"], - ["unstable", "1.2.4-7.unstable.r34640000001.gabcdef0"], - ] as const)("accepts a %s runtime version: %s", (channel, runtimeVersion) => { - expect(() => - validateRuntimeReleaseIdentity({ - channel, - runId: runtime.run_id, - sha: runtime.sha, - version: runtimeVersion, - }) - ).not.toThrow(); - }); - - it.each([ - ["wrong channel", "unstable", "1.2.4-canary.r34640000001.gabcdef0.signed"], - ["test suffix", "unstable", "1.2.4-unstable.r34640000001.gabcdef0.test"], - ["canary test suffix", "canary", "1.2.4-canary.r34640000001.gabcdef0.test"], - ["old numeric run", "unstable", "1.2.4-unstable.34640000001.gabcdef0"], - ["missing r", "unstable", "1.2.4-unstable.34640000001.gabcdef0"], - ["mismatched run", "unstable", "1.2.4-unstable.r34640000002.gabcdef0"], - ["mismatched sha", "unstable", "1.2.4-unstable.r34640000001.g1234567"], - ["build metadata", "unstable", "1.2.4-unstable.r34640000001.gabcdef0+build.42"], - ["canary without signing", "canary", "1.2.4-canary.r34640000001.gabcdef0"], - [ - "canary extra signing suffix", - "canary", - "1.2.4-canary.r34640000001.gabcdef0.signed.extra", - ], - ["unstable signed", "unstable", "1.2.4-unstable.r34640000001.gabcdef0.signed"], - [ - "non-numeric baseline prerelease", - "unstable", - "1.2.4-preview.unstable.r34640000001.gabcdef0", - ], - ["not a prerelease", "unstable", "1.2.4"], - ["surrounding whitespace", "unstable", " 1.2.4-unstable.r34640000001.gabcdef0"], - ] as const)("rejects %s: %s", (_name, channel, runtimeVersion) => { - expect(() => - validateRuntimeReleaseIdentity({ - channel, - runId: runtime.run_id, - sha: runtime.sha, - version: runtimeVersion, - }) - ).toThrow(); - }); -}); - -describe("release dispatch", () => { - it.each(["latest", "prerelease", "unstable"] as const)( - "accepts direct %s publication", - (distTag) => { - expect(validateReleaseDispatch({ ...inputs, distTag })).toEqual({ - kind: "direct", - runtimeRunId: "", - runtimeSha: "", - runtimeVersion: "", - testPolicy: "required", - }); - } - ); - - it("accepts direct unstable dry-run", () => { - expect( - validateReleaseDispatch({ ...inputs, distTag: "unstable", mode: "dry-run" }).kind - ).toBe("direct"); - }); - - it.each([ - ["canary", "dry-run", "1.0.83-5.canary.r34640000001.gabcdef0.unsigned"], - ["canary", "publish", "1.0.83-5.canary.r34640000001.gabcdef0.signed"], - ["unstable", "dry-run", runtime.version], - ["unstable", "publish", runtime.version], - ] as const)("accepts runtime-backed %s %s", (distTag, mode, version) => { - expect( - validateReleaseDispatch({ - ...inputs, - distTag, - mode, - runtimeJson: JSON.stringify({ ...runtime, version }), - }) - ).toEqual({ - kind: "runtime", - runtimeRunId: runtime.run_id, - runtimeSha: runtime.sha, - runtimeVersion: version, - testPolicy: "required", - }); - }); - - it.each(["required", "advisory", "skipped"] as const)( - "accepts runtime-backed releases with %s tests", - (testPolicy) => { - expect( - validateReleaseDispatch({ - ...inputs, - runtimeJson: JSON.stringify(runtime), - testPolicy, - }).testPolicy - ).toBe(testPolicy); - } - ); - - it("rejects canary without runtime JSON", () => { - expect(() => validateReleaseDispatch({ ...inputs, distTag: "canary" })).toThrow( - "Canary releases require runtime JSON" - ); - }); - - it.each(["latest", "prerelease"] as const)("rejects %s dry-run", (distTag) => { - expect(() => validateReleaseDispatch({ ...inputs, distTag, mode: "dry-run" })).toThrow( - "Dry-run mode is supported only for canary and unstable releases" - ); - }); - - it.each(["latest", "prerelease"] as const)("rejects runtime JSON for %s", (distTag) => { - expect(() => - validateReleaseDispatch({ - ...inputs, - distTag, - runtimeJson: JSON.stringify(runtime), - }) - ).toThrow("Runtime JSON is supported only for canary and unstable releases"); - }); - - it("rejects a direct version with runtime JSON", () => { - expect(() => - validateReleaseDispatch({ - ...inputs, - runtimeJson: JSON.stringify(runtime), - version: "2.0.0-unstable.manual", - }) - ).toThrow("direct version input"); - }); - - it.each(["advisory", "skipped"] as const)( - "rejects %s tests for direct releases", - (testPolicy) => { - expect(() => validateReleaseDispatch({ ...inputs, testPolicy })).toThrow( - "Direct releases require the default runtime E2E test policy" - ); - } - ); - - it("rejects unknown dist-tags, modes, and test policies", () => { - expect(() => - validateReleaseDispatch({ ...inputs, distTag: "preview" as "unstable" }) - ).toThrow("Invalid release dist-tag"); - expect(() => validateReleaseDispatch({ ...inputs, mode: "test" as "publish" })).toThrow( - "Invalid release mode" - ); - expect(() => - validateReleaseDispatch({ ...inputs, testPolicy: "optional" as "required" }) - ).toThrow("Invalid runtime E2E test policy"); - }); - - it.each([ - ["malformed JSON", "{"], - ["whitespace-only input", " "], - ["surrounding whitespace", ` ${JSON.stringify(runtime)}`], - ["null", "null"], - ["array", "[]"], - ["missing key", JSON.stringify({ version: runtime.version, sha: runtime.sha })], - ["extra key", JSON.stringify({ ...runtime, source: "github-packages" })], - ["non-string field", JSON.stringify({ ...runtime, run_id: 123 })], - ["zero run ID", JSON.stringify({ ...runtime, run_id: "0" })], - ["non-canonical run ID", JSON.stringify({ ...runtime, run_id: "0123" })], - ["uppercase SHA", JSON.stringify({ ...runtime, sha: runtime.sha.toUpperCase() })], - [ - "wrong channel", - JSON.stringify({ - ...runtime, - version: "1.0.83-5.canary.r34640000001.gabcdef0.signed", - }), - ], - [ - "mismatched version run ID", - JSON.stringify({ - ...runtime, - version: "1.0.83-5.unstable.r34640000002.gabcdef0", - }), - ], - [ - "mismatched version SHA", - JSON.stringify({ - ...runtime, - version: "1.0.83-5.unstable.r34640000001.g1234567", - }), - ], - [ - "obsolete test version", - JSON.stringify({ - ...runtime, - version: "1.0.83-5.unstable.r34640000001.gabcdef0.test", - }), - ], - ])("rejects %s", (_name, runtimeJson) => { - expect(() => validateReleaseDispatch({ ...inputs, runtimeJson })).toThrow(); - }); -}); diff --git a/nodejs/test/runtimeArtifacts.test.ts b/nodejs/test/runtimeArtifacts.test.ts index 24eef78797..9b00a59f09 100644 --- a/nodejs/test/runtimeArtifacts.test.ts +++ b/nodejs/test/runtimeArtifacts.test.ts @@ -12,7 +12,7 @@ import { dirname, join, resolve } from "node:path"; import { createHash } from "node:crypto"; import { spawnSync } from "node:child_process"; import { c as createTar } from "tar"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, onTestFinished, vi } from "vitest"; import { defaultRuntimeCacheRoot, @@ -23,7 +23,7 @@ import { materializeRuntimeBundle, } from "../src/runtimeArtifacts.js"; import { COPILOT_CLI_USE_NPM_PACKAGE, COPILOT_CLI_VERSION } from "../src/cliVersion.js"; -import { ensureCopilotPackage } from "../scripts/releaseArtifacts.js"; +import { downloadVerifiedReleaseAsset, ensureCopilotPackage } from "../scripts/releaseArtifacts.js"; import { getLegacyCliPathForTests } from "./e2e/harness/sdkTestContext.js"; import * as runtimeLayout from "../../scripts/runtime-layout.mjs"; @@ -172,6 +172,7 @@ describe("materializeRuntimeBundle", () => { mkdirSync(join(sourceDir, "preloads"), { recursive: true }); writeFileSync(join(sourceDir, "preloads", "extension_bootstrap.mjs"), "bootstrap"); writeFileSync(join(sourceDir, "app.js"), "excluded"); + writeFileSync(join(sourceDir, "cli-main.js"), "excluded"); writeFileSync(join(sourceDir, "copilot"), "excluded"); writeFileSync(join(sourceDir, "copilot.exe"), "excluded"); writeFileSync(join(sourceDir, "LICENSE.md"), "excluded"); @@ -206,6 +207,7 @@ describe("materializeRuntimeBundle", () => { "bootstrap" ); expect(existsSync(join(installDir, "app.js"))).toBe(false); + expect(existsSync(join(installDir, "cli-main.js"))).toBe(false); expect(existsSync(join(installDir, "copilot"))).toBe(false); expect(existsSync(join(installDir, "copilot.exe"))).toBe(false); expect(existsSync(join(installDir, "LICENSE.md"))).toBe(false); @@ -414,48 +416,73 @@ describe("release package acquisition", () => { expect(fetcher).toHaveBeenCalledTimes(2); }); - it("downloads, verifies, and caches a release package for packaging", async () => { - const sourceRoot = mkdtempSync(join(tmpdir(), "copilot-release-source-")); - const packageRoot = join(sourceRoot, "package"); - const platform = "linux-x64"; - const prebuilds = join(packageRoot, "prebuilds", platform); - mkdirSync(prebuilds, { recursive: true }); - writeFileSync(join(prebuilds, "copilot-runtime"), "wrapper"); - writeFileSync(join(prebuilds, "runtime.node"), "runtime"); - mkdirSync(join(packageRoot, "schemas"), { recursive: true }); - writeFileSync(join(packageRoot, "schemas", "api.schema.json"), "{}"); - - const archivePath = join(sourceRoot, "runtime.tgz"); - await createTar({ cwd: sourceRoot, file: archivePath, gzip: true }, ["package"]); - const archive = readFileSync(archivePath); - const version = "1.2.3-4"; - const assetName = getRuntimeReleaseAssetName(version, platform); - const checksum = createHash("sha256").update(archive).digest("hex"); - const fetcher = vi.fn(async (input: string | URL | Request) => - String(input).endsWith("/SHA256SUMS.txt") - ? new Response(`${checksum} ${assetName}\n`) - : new Response(archive) - ); - const cacheRoot = join(sourceRoot, "cache"); - - const downloadedPackage = await ensureCopilotPackage(version, { - cacheRoot, - fetch: fetcher, - platform, - }); - expect(readFileSync(join(downloadedPackage, "schemas", "api.schema.json"), "utf8")).toBe( - "{}" - ); - - await expect( - ensureCopilotPackage(version, { - cacheRoot, - fetch: fetcher, - platform, - }) - ).resolves.toBe(downloadedPackage); - expect(fetcher).toHaveBeenCalledTimes(2); - }); + it.each([ + ["7.8.9", "copilot-cli", "v7.8.9"], + ["7.8.9-2", "copilot-cli", "v7.8.9-2"], + ["7.8.9-unstable.r123.gabcdef0", "copilot-sdk", "runtime-7.8.9-unstable.r123.gabcdef0"], + ["7.8.9-2.unstable.r123.gabcdef0", "copilot-sdk", "runtime-7.8.9-2.unstable.r123.gabcdef0"], + ])( + "downloads, verifies, and caches %s for source packaging", + async (version, repository, tag) => { + const sourceRoot = mkdtempSync(join(tmpdir(), "copilot-release-source-")); + onTestFinished(() => rmSync(sourceRoot, { recursive: true, force: true })); + const packageRoot = join(sourceRoot, "package"); + const platform = "linux-x64"; + const prebuilds = join(packageRoot, "prebuilds", platform); + mkdirSync(prebuilds, { recursive: true }); + writeFileSync(join(prebuilds, "copilot-runtime"), "wrapper"); + writeFileSync(join(prebuilds, "runtime.node"), "runtime"); + mkdirSync(join(packageRoot, "schemas"), { recursive: true }); + writeFileSync(join(packageRoot, "schemas", "api.schema.json"), "{}"); + + const archivePath = join(sourceRoot, "runtime.tgz"); + await createTar({ cwd: sourceRoot, file: archivePath, gzip: true }, ["package"]); + const archive = readFileSync(archivePath); + const assetName = getRuntimeReleaseAssetName(version, platform); + const checksum = createHash("sha256").update(archive).digest("hex"); + vi.stubEnv("COPILOT_CLI_DOWNLOAD_BASE_URL", "https://ambient.example.invalid/releases"); + onTestFinished(() => vi.unstubAllEnvs()); + for (const mirror of [undefined, "https://mirror.example.invalid/releases///"]) { + const base = + mirror?.replace(/\/+$/, "") ?? + `https://github.com/github/${repository}/releases/download`; + const releaseUrl = `${base}/${tag}`; + const fetcher = vi.fn(async (input: string | URL | Request) => { + const url = String(input); + if (url === `${releaseUrl}/SHA256SUMS.txt`) + return new Response(`${checksum} ${assetName}\n`); + if (url === `${releaseUrl}/${assetName}`) return new Response(archive); + return new Response("Unexpected release URL", { status: 404 }); + }); + const environment = { COPILOT_CLI_DOWNLOAD_BASE_URL: mirror }; + const cacheRoot = join(sourceRoot, mirror ? "mirror-cache" : "cache"); + await expect( + downloadVerifiedReleaseAsset(version, assetName, { + environment, + fetch: fetcher, + }) + ).resolves.toEqual(archive); + const options = { + cacheRoot, + fetch: fetcher, + platform, + environment, + }; + const downloadedPackage = await ensureCopilotPackage(version, options); + expect( + readFileSync(join(downloadedPackage, "schemas", "api.schema.json"), "utf8") + ).toBe("{}"); + await expect(ensureCopilotPackage(version, options)).resolves.toBe( + downloadedPackage + ); + expect(fetcher.mock.calls.map(([url]) => String(url))).toEqual([ + `${releaseUrl}/SHA256SUMS.txt`, + `${releaseUrl}/${assetName}`, + `${releaseUrl}/${assetName}`, + ]); + } + } + ); it("rejects a release package that does not match SHA256SUMS.txt", async () => { const cacheRoot = mkdtempSync(join(tmpdir(), "copilot-release-mismatch-")); diff --git a/nodejs/test/rust-codegen.test.ts b/nodejs/test/rust-codegen.test.ts index d38141cab6..0a169c0825 100644 --- a/nodejs/test/rust-codegen.test.ts +++ b/nodejs/test/rust-codegen.test.ts @@ -13,23 +13,85 @@ import { generateApiTypesCode, generateRpcCode, generateSessionEventsCode, - isRustCodegenEntrypoint, } from "../../scripts/codegen/rust.ts"; import { legacyRequestSchema } from "./legacy-parameters-fixture.ts"; -describe("Rust codegen entrypoint", () => { - it("matches Windows paths case-insensitively", () => { - expect( - isRustCodegenEntrypoint( - "C:\\a\\copilot-agent-runtime\\src\\sdk\\scripts\\codegen\\rust.ts", - "c:\\a\\copilot-agent-runtime\\src\\sdk\\scripts\\codegen\\rust.ts", - "win32" - ) - ).toBe(true); +describe("Rust API type codegen", () => { + it("does not emit MCP list request types for a parameterless endpoint", () => { + const code = generateApiTypesCode({ + session: { + mcp: { + list: { + rpcMethod: "session.mcp.list", + params: null, + }, + }, + }, + }); + expect(code).not.toContain("SessionMcpListParams"); + expect(code).not.toContain("McpListRequest"); + }); + + it.each([ + { + sentinel: { type: "null" }, + expectedAlias: "pub type SessionAccountResult = Option;", + }, + // `{ "not": {} }` is the omission sentinel, not a wire null, so the + // alias must not become Option and accept/serialize a null. + { sentinel: { not: {} }, expectedAlias: "pub type SessionAccountResult = SessionAccount;" }, + ])( + "aliases a named reference according to its null-like branch (%j)", + ({ sentinel, expectedAlias }) => { + const code = generateApiTypesCode({ + definitions: { + SessionAccount: { + type: "object", + title: "SessionAccount", + properties: { accountId: { type: "string" } }, + required: ["accountId"], + }, + SessionAccountResult: { + title: "SessionAccountResult", + anyOf: [{ $ref: "#/definitions/SessionAccount" }, sentinel], + }, + }, + session: { + accounts: { + getCurrent: { + rpcMethod: "session.accounts.getCurrent", + params: null, + result: { $ref: "#/definitions/SessionAccountResult" }, + }, + }, + }, + } as ApiSchema); + + expect(code).toContain(expectedAlias); + expect(code).toContain("pub struct SessionAccount {"); + expect(code).not.toContain("pub struct SessionAccountResult {"); + } + ); + it("preserves future customization reload enum values in both directions", () => { + const code = generateApiTypesCode({ + definitions: { + CustomizationReloadStatus: { type: "string", enum: ["reloaded", "skipped"] }, + CustomizationReloadSubsystem: { type: "string", enum: ["skills", "hooks"] }, + ClosedStatus: { type: "string", enum: ["ready"] }, + }, + } as ApiSchema); + + for (const name of ["CustomizationReloadStatus", "CustomizationReloadSubsystem"]) { + expect(code).toContain(`pub enum ${name} {`); + expect(code).toContain(`impl From for ${name} {`); + expect(code).toContain(`impl From<${name}> for String {`); + } + expect(code).toContain('#[serde(from = "String", into = "String")]'); + expect(code).toContain("Unknown(String),"); + expect(code).toContain("pub enum ClosedStatus {"); + expect(code).toContain("#[serde(other)]"); }); -}); -describe("Rust API type codegen", () => { it.each(["anyOf", "oneOf"] as const)( "keeps required phase results typed through %s references", (keyword) => { @@ -276,6 +338,54 @@ pub struct Container`); expect(code).toContain("False(UnsupportedEnqueueCommandResult)"); }); + it("reuses the get-sources result for instructions reload without generating an unused type", () => { + const schema = { + definitions: { + InstructionsGetSourcesResult: { + type: "object", + properties: { sources: { type: "array", items: { type: "string" } } }, + required: ["sources"], + }, + OtherResult: { + type: "object", + properties: { sources: { type: "array", items: { type: "string" } } }, + required: ["sources"], + }, + }, + session: { + instructions: { + getSources: { + rpcMethod: "session.instructions.getSources", + params: null, + result: { $ref: "#/definitions/InstructionsGetSourcesResult" }, + }, + reload: { + rpcMethod: "session.instructions.reload", + params: null, + result: { $ref: "#/definitions/InstructionsGetSourcesResult" }, + }, + }, + }, + }; + const apiSchema = schema as ApiSchema; + + const types = generateApiTypesCode(apiSchema); + const rpc = generateRpcCode(apiSchema); + expect(types).toContain("pub struct InstructionsGetSourcesResult {"); + expect(types).not.toContain("pub struct SessionInstructionsReloadResult {"); + expect(rpc).toContain( + "pub async fn reload(&self) -> Result" + ); + + schema.session.instructions.reload.result.$ref = "#/definitions/OtherResult"; + expect(generateApiTypesCode(apiSchema)).toContain( + "pub struct SessionInstructionsReloadResult {" + ); + expect(generateRpcCode(apiSchema)).toContain( + "pub async fn reload(&self) -> Result" + ); + }); + it.each([true, false])( "validates a reference's sibling constant without changing its enum (required: %s)", (required) => { @@ -563,6 +673,49 @@ pub enum ${name} { expect(empty).not.toContain("pub type SessionSearchData"); }); + it("types the durable human response event union", () => { + const response: JSONSchema7 = { + title: "HumanResponseRecordedResponse", + anyOf: [ + { + type: "object", + required: ["responseKind", "answer"], + properties: { + responseKind: { type: "string", const: "user_input" }, + answer: { type: "string" }, + }, + }, + { + type: "object", + required: ["responseKind", "approved"], + properties: { + responseKind: { type: "string", const: "exit_plan_mode" }, + approved: { type: "boolean" }, + }, + }, + ], + }; + const code = generateSessionEventsCode( + eventSchema( + { + type: "object", + required: ["response"], + properties: { + response: { $ref: "#/definitions/HumanResponseRecordedResponse" }, + }, + }, + { HumanResponseRecordedResponse: response } + ) + ); + + expect(code).toContain("pub response: HumanResponseRecordedResponse,"); + expect(code).toContain(`pub enum HumanResponseRecordedResponse { + UserInput(HumanResponseRecordedResponseUserInput), + ExitPlanMode(HumanResponseRecordedResponseExitPlanMode), +}`); + expect(code).not.toContain("pub response: serde_json::Value,"); + }); + it.each(["reasonCode", "judgeStatus", "evaluationStage"])( "preserves explicit unknown values in nested approval %s enums", (property) => { diff --git a/nodejs/test/session-event-codegen.test.ts b/nodejs/test/session-event-codegen.test.ts index 60906720de..c18f190bda 100644 --- a/nodejs/test/session-event-codegen.test.ts +++ b/nodejs/test/session-event-codegen.test.ts @@ -2,7 +2,11 @@ import type { JSONSchema7 } from "json-schema"; import { describe, expect, it } from "vitest"; import { generateSessionEventsCode as generateCSharpSessionEventsCode } from "../../scripts/codegen/csharp.ts"; -import { generateGoSessionEventsCode } from "../../scripts/codegen/go.ts"; +import { + collectGoDeprecatedEventNames, + generateGoSessionEventAliasFile, + generateGoSessionEventsCode, +} from "../../scripts/codegen/go.ts"; import { generatePythonSessionEventsCode, postProcessExternalRefsForPython, @@ -10,6 +14,67 @@ import { import { generateSessionEventsCode as generateRustSessionEventsCode } from "../../scripts/codegen/rust.ts"; describe("session event codegen", () => { + it.each([ + ["inline", "legacy"], + ["reference", "legacy"], + ["inline", "someEvent"], + ["reference", "someEvent"], + ])("deprecates the whole %s %s event across SDK projections", (shape, eventName) => { + const event = { + type: "object", + deprecated: true, + required: ["type", "data"], + properties: { + type: { const: `tool.${eventName}` }, + data: { type: "object", properties: { text: { type: "string" } } }, + }, + } satisfies JSONSchema7 & { deprecated: boolean }; + const schema: JSONSchema7 = { + definitions: { + LegacyEvent: event, + SessionEvent: { + anyOf: [shape === "inline" ? event : { $ref: "#/definitions/LegacyEvent" }], + }, + }, + }; + + const className = `Tool${eventName[0].toUpperCase()}${eventName.slice(1)}`; + expect(generateCSharpSessionEventsCode(schema)).toMatch( + new RegExp( + `\\[Obsolete\\([^\\n]+\\)\\]\\n#endif\\npublic sealed partial class ${className}Event` + ) + ); + const { typeCode } = generateGoSessionEventsCode(schema, "rpc"); + expect(typeCode).toMatch( + new RegExp(`// Deprecated:[^\\n]+\\n\\s*SessionEventType${className} `) + ); + expect(typeCode).toMatch( + new RegExp(`//\\n// Deprecated:[^\\n]+\\ntype ${className}Data struct`) + ); + const aliases = generateGoSessionEventAliasFile( + typeCode, + [], + [], + [], + collectGoDeprecatedEventNames(schema) + ); + expect(aliases).toMatch( + new RegExp(`// Deprecated:[^\\n]+\\n\\s*${className}Data = rpc\\.${className}Data`) + ); + expect(aliases).toMatch( + new RegExp(`// Deprecated:[^\\n]+\\n\\s*SessionEventType${className} = rpc\\.`) + ); + const rust = generateRustSessionEventsCode(schema); + expect( + rust.match( + new RegExp( + `#\\[deprecated\\]\\n\\s*#\\[serde\\(rename = "tool\\.${eventName}"\\)\\]`, + "g" + ) + ) + ).toHaveLength(2); + }); + describe.each(["anyOf", "oneOf"] as const)("Go root %s payloads", (unionKeyword) => { it.each(["inline", "local", "shared"])( "preserves %s union payloads as raw JSON", diff --git a/nodejs/test/session-event-types.test.ts b/nodejs/test/session-event-types.test.ts index 330b381090..de03f98142 100644 --- a/nodejs/test/session-event-types.test.ts +++ b/nodejs/test/session-event-types.test.ts @@ -44,6 +44,8 @@ import type { ResumeData, StartData, ToolExecutionCompleteData, + ToolExecutionCompleteFileEdit, + ToolExecutionCompleteFileEditKind, ToolExecutionPartialData, ToolExecutionProgressData, ToolExecutionStartData, @@ -220,6 +222,21 @@ describe("Session event type exports (#1156)", () => { expect(data.turnId).toBe("turn-1"); }); + it.each([ + "create", + "edit", + "delete", + "future_kind", + ] satisfies ToolExecutionCompleteFileEditKind[])( + "preserves file edit kind %s in completion data", + (kind) => { + const edit: ToolExecutionCompleteFileEdit = { path: "/session/file.txt", kind }; + const data: Pick = { fileEdits: [edit] }; + + expect(JSON.parse(JSON.stringify(data))).toEqual({ fileEdits: [edit] }); + } + ); + it("exposes explicit user approval metadata for managed Domain requests", () => { const request: PermissionRequest = { kind: "url", diff --git a/nodejs/test/session_fs_adapter.test.ts b/nodejs/test/session_fs_adapter.test.ts index 98749dffba..49591f044c 100644 --- a/nodejs/test/session_fs_adapter.test.ts +++ b/nodejs/test/session_fs_adapter.test.ts @@ -4,7 +4,11 @@ import { MemoryProvider } from "@platformatic/vfs"; import { describe, expect, it } from "vitest"; -import { createSessionFsAdapter, type SessionFsProvider } from "../src/index.js"; +import { + createSessionFsAdapter, + SessionFsWriteFailure, + type SessionFsProvider, +} from "../src/index.js"; describe("SessionFsAdapter", () => { it("should map all sessionFs handler operations", async () => { @@ -16,6 +20,12 @@ describe("SessionFsAdapter", () => { async readFile(path) { return (await memoryProvider.readFile(sp(path), "utf8")) as string; }, + async readFileBytes(path) { + return memoryProvider.readFile(sp(path)); + }, + async writeFileBytes(path, content) { + await memoryProvider.writeFile(sp(path), content); + }, async writeFile(path, content) { await memoryProvider.writeFile(sp(path), content); }, @@ -126,6 +136,76 @@ describe("SessionFsAdapter", () => { expect(content.content).toBe("hello world"); expect(content.error).toBeUndefined(); + await memoryProvider.writeFile( + sp("/workspace/nested/photo.png"), + Buffer.from([0, 255, 254, 1]) + ); + const binary = await handler.readFileBytes({ + sessionId, + path: "/workspace/nested/photo.png", + }); + expect(binary.content).toBe("AP/+AQ=="); + expect(binary.error).toBeUndefined(); + const written = await handler.writeFileBytes({ + sessionId, + path: "/workspace/nested/written.bin", + content: "AP/+AQ==", + mode: 0o600, + }); + expect(written).toBeUndefined(); + expect(await memoryProvider.readFile(sp("/workspace/nested/written.bin"))).toEqual( + Buffer.from([0, 255, 254, 1]) + ); + expect( + await handler.writeFileBytes({ + sessionId, + path: "/workspace/nested/invalid.bin", + content: "AA==AAAA", + }) + ).toMatchObject({ + code: "UNKNOWN", + message: "invalid sessionFs.writeFileBytes base64 content", + }); + expect(await memoryProvider.exists(sp("/workspace/nested/invalid.bin"))).toBe(false); + expect( + await handler.writeFileBytes({ + sessionId, + path: "/workspace/nested/large.bin", + content: "A".repeat(64 * 1024 * 1024), + }) + ).toMatchObject({ + code: "UNKNOWN", + message: "sessionFs.writeFileBytes content exceeds the binary write limit", + }); + expect(await memoryProvider.exists(sp("/workspace/nested/large.bin"))).toBe(false); + const missingBinary = await createSessionFsAdapter({ + ...provider, + readFileBytes: undefined, + }).readFileBytes({ + sessionId, + path: "/workspace/nested/photo.png", + }); + expect(missingBinary.error?.code).toBe("UNKNOWN"); + expect( + await createSessionFsAdapter({ ...provider, writeFileBytes: undefined }).writeFileBytes( + { + sessionId, + path: "/workspace/nested/missing.bin", + content: "AP/+AQ==", + } + ) + ).toMatchObject({ code: "UNKNOWN" }); + + const oversized = await createSessionFsAdapter({ + ...provider, + readFileBytes: async () => new Uint8Array(((64 * 1024 * 1024 - 1024) / 4) * 3 + 1), + }).readFileBytes({ sessionId, path: "/workspace/nested/photo.png" }); + expect(oversized.content).toBe(""); + expect(oversized.error).toEqual({ + code: "UNKNOWN", + message: "sessionFs.readFileBytes content exceeds the binary read limit", + }); + const entries = await handler.readdir({ sessionId, path: "/workspace/nested" }); expect(entries.entries).toContain("file.txt"); expect(entries.error).toBeUndefined(); @@ -275,5 +355,22 @@ describe("SessionFsAdapter", () => { }); expect(unknownError).toBeDefined(); expect(unknownError!.code).toBe("UNKNOWN"); + + const changedProvider = createSessionFsAdapter( + makeThrowingProvider(new SessionFsWriteFailure("write truncated the target")) + ); + expect( + await changedProvider.writeFile({ sessionId, path: "changed.txt", content: "content" }) + ).toEqual({ + code: "UNKNOWN", + message: "write truncated the target", + writeChanged: true, + }); + expect( + await changedProvider.appendFile({ sessionId, path: "changed.txt", content: "content" }) + ).not.toHaveProperty("writeChanged"); + expect( + await handler.writeFile({ sessionId, path: "missing.txt", content: "content" }) + ).not.toHaveProperty("writeChanged"); }); }); diff --git a/nodejs/test/set-tools.test.ts b/nodejs/test/set-tools.test.ts new file mode 100644 index 0000000000..689f2d52ce --- /dev/null +++ b/nodejs/test/set-tools.test.ts @@ -0,0 +1,163 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, expect, it, vi } from "vitest"; +import { z } from "zod"; +import { CopilotSession } from "../src/session.js"; +import { defineTool } from "../src/types.js"; + +type Deferred = { resolve: (value?: unknown) => void; reject: (error: unknown) => void }; + +/** + * A session whose `session.tools.set` requests stay pending until the test + * settles them, and whose other requests succeed immediately. + */ +function sessionWithPendingToolsSet() { + const pending: Deferred[] = []; + const sendRequest = vi.fn((method: string) => + method === "session.tools.set" + ? new Promise((resolve, reject) => pending.push({ resolve, reject })) + : Promise.resolve(undefined) + ); + const session = new CopilotSession("session-1", { sendRequest } as never); + const toolsSetCalls = () => + sendRequest.mock.calls.filter(([method]) => method === "session.tools.set"); + return { session, sendRequest, pending, toolsSetCalls }; +} + +function labelTool(name: string, handler: () => string) { + return defineTool(name, { + description: `${name} tool`, + parameters: z.object({}), + handler, + }); +} + +async function requestTool(session: CopilotSession, requestId: string, toolName: string) { + (session as any)._handleBroadcastEvent({ + type: "external_tool.requested", + data: { + requestId, + sessionId: "session-1", + toolCallId: `call-${requestId}`, + toolName, + arguments: {}, + }, + }); + await new Promise((resolve) => setImmediate(resolve)); +} + +describe("CopilotSession.setTools", () => { + it("sends the complete tool set and switches handlers once the runtime accepts", async () => { + const { session, pending, toolsSetCalls } = sessionWithPendingToolsSet(); + const oldHandler = vi.fn(() => "old"); + const newHandler = vi.fn(() => "new"); + session.registerTools([labelTool("old_tool", oldHandler)]); + + const replacement = session.setTools([ + labelTool("new_tool", newHandler), + defineTool("declared_tool", { description: "Serviced by another client" }), + ]); + await vi.waitFor(() => expect(toolsSetCalls()).toHaveLength(1)); + expect(toolsSetCalls()[0][1]).toEqual({ + sessionId: "session-1", + tools: [ + expect.objectContaining({ + name: "new_tool", + description: "new_tool tool", + parameters: expect.objectContaining({ type: "object" }), + }), + expect.objectContaining({ + name: "declared_tool", + description: "Serviced by another client", + }), + ], + }); + + // Until the runtime accepts, the previous handlers stay in effect. + await requestTool(session, "before-new", "new_tool"); + await requestTool(session, "before-old", "old_tool"); + expect(newHandler).not.toHaveBeenCalled(); + expect(oldHandler).toHaveBeenCalledTimes(1); + + pending[0].resolve({}); + await replacement; + + await requestTool(session, "after-new", "new_tool"); + await requestTool(session, "after-old", "old_tool"); + await requestTool(session, "after-declared", "declared_tool"); + expect(newHandler).toHaveBeenCalledTimes(1); + expect(oldHandler).toHaveBeenCalledTimes(1); + }); + + it("keeps the previous handlers when the runtime rejects the replacement", async () => { + const { session, pending } = sessionWithPendingToolsSet(); + const oldHandler = vi.fn(() => "old"); + const newHandler = vi.fn(() => "new"); + session.registerTools([labelTool("old_tool", oldHandler)]); + + const replacement = session.setTools([labelTool("new_tool", newHandler)]); + await vi.waitFor(() => expect(pending).toHaveLength(1)); + pending[0].reject(new Error("External tool name clash: new_tool")); + await expect(replacement).rejects.toThrow("External tool name clash"); + + await requestTool(session, "rejected-new", "new_tool"); + await requestTool(session, "kept-old", "old_tool"); + expect(newHandler).not.toHaveBeenCalled(); + expect(oldHandler).toHaveBeenCalledTimes(1); + }); + + it("removes this client's tools when given an empty set", async () => { + const { session, pending, toolsSetCalls } = sessionWithPendingToolsSet(); + const oldHandler = vi.fn(() => "old"); + session.registerTools([labelTool("old_tool", oldHandler)]); + + const replacement = session.setTools([]); + await vi.waitFor(() => expect(pending).toHaveLength(1)); + expect(toolsSetCalls()[0][1]).toEqual({ sessionId: "session-1", tools: [] }); + pending[0].resolve({}); + await replacement; + + await requestTool(session, "removed", "old_tool"); + expect(oldHandler).not.toHaveBeenCalled(); + }); + + it("applies concurrent replacements one at a time, in call order", async () => { + const { session, pending, toolsSetCalls } = sessionWithPendingToolsSet(); + const firstHandler = vi.fn(() => "first"); + const secondHandler = vi.fn(() => "second"); + + const first = session.setTools([labelTool("first_tool", firstHandler)]); + const second = session.setTools([labelTool("second_tool", secondHandler)]); + await vi.waitFor(() => expect(pending).toHaveLength(1)); + await new Promise((resolve) => setImmediate(resolve)); + expect(toolsSetCalls()).toHaveLength(1); + + pending[0].resolve({}); + await first; + await vi.waitFor(() => expect(pending).toHaveLength(2)); + expect(toolsSetCalls()[1][1]).toMatchObject({ tools: [{ name: "second_tool" }] }); + pending[1].resolve({}); + await second; + + await requestTool(session, "first", "first_tool"); + await requestTool(session, "second", "second_tool"); + expect(firstHandler).not.toHaveBeenCalled(); + expect(secondHandler).toHaveBeenCalledTimes(1); + }); + + it("still applies a later replacement after an earlier one is rejected", async () => { + const { session, pending } = sessionWithPendingToolsSet(); + const secondHandler = vi.fn(() => "second"); + + const first = session.setTools([labelTool("bad.name", () => "bad")]); + const second = session.setTools([labelTool("second_tool", secondHandler)]); + await vi.waitFor(() => expect(pending).toHaveLength(1)); + pending[0].reject(new Error("contains invalid characters")); + await expect(first).rejects.toThrow("invalid characters"); + await vi.waitFor(() => expect(pending).toHaveLength(2)); + pending[1].resolve({}); + await second; + + await requestTool(session, "second", "second_tool"); + expect(secondHandler).toHaveBeenCalledTimes(1); + }); +}); diff --git a/nodejs/test/shared-codegen.test.ts b/nodejs/test/shared-codegen.test.ts index 0177fd209c..91e29d07d0 100644 --- a/nodejs/test/shared-codegen.test.ts +++ b/nodejs/test/shared-codegen.test.ts @@ -1,7 +1,9 @@ import type { JSONSchema7 } from "json-schema"; -import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { spawnSync } from "node:child_process"; +import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { fileURLToPath } from "node:url"; import { describe, expect, it, onTestFinished } from "vitest"; import { @@ -11,12 +13,76 @@ import { findSharedSchemaDefinitions, getEnumValueDescriptions, inlineExternalSchemaDefinitions, + isCodegenEntrypoint, isIntegerSchemaBoundedToInt32, rewriteSharedDefinitionReferences, resolveCopilotSchemaPaths, } from "../../scripts/codegen/utils.ts"; +describe("codegen entrypoints", () => { + it("recognizes Windows entrypoint paths case-insensitively", () => { + expect( + isCodegenEntrypoint("C:\\b\\execroot\\python.ts", "c:\\B\\execroot\\python.ts", "win32") + ).toBe(true); + }); + + it("rejects other modules and missing entrypoints", () => { + expect(isCodegenEntrypoint(undefined, "python.ts")).toBe(false); + expect(isCodegenEntrypoint("go.ts", "python.ts")).toBe(false); + expect(isCodegenEntrypoint("PYTHON.ts", "python.ts", "linux")).toBe(false); + }); + + it("recognizes a Bazel-style directory symlink to the generator", async () => { + const root = await mkdtemp(join(tmpdir(), "copilot-codegen-entrypoint-")); + onTestFinished(() => rm(root, { recursive: true, force: true })); + const source = join(root, "source"); + const execroot = join(root, "execroot"); + await mkdir(source); + await writeFile(join(source, "python.ts"), ""); + await writeFile(join(source, "other.ts"), ""); + await symlink(source, execroot, process.platform === "win32" ? "junction" : "dir"); + expect(isCodegenEntrypoint(join(execroot, "python.ts"), join(source, "python.ts"))).toBe( + true + ); + expect(isCodegenEntrypoint(join(execroot, "other.ts"), join(source, "python.ts"))).toBe( + false + ); + expect(isCodegenEntrypoint("", join(source, "python.ts"))).toBe(false); + }); +}); + describe("shared schema definition codegen utilities", () => { + it.each(["typescript", "python", "go", "csharp"])( + "runs the %s generator through a linked entrypoint", + async (language) => { + const root = await mkdtemp(join(tmpdir(), "copilot-codegen-entrypoint-")); + onTestFinished(() => rm(root, { recursive: true, force: true })); + const codegenRoot = fileURLToPath(new URL("../../scripts/codegen", import.meta.url)); + const linkedRoot = join(root, "codegen"); + await symlink( + codegenRoot, + linkedRoot, + process.platform === "win32" ? "junction" : "dir" + ); + const missingSchema = join(root, "missing-schema.json"); + const result = spawnSync( + process.execPath, + [ + join(codegenRoot, "node_modules", "tsx", "dist", "cli.mjs"), + join(linkedRoot, `${language}.ts`), + missingSchema, + missingSchema, + ], + { cwd: codegenRoot, encoding: "utf8", timeout: 30_000 } + ); + + expect(result.error).toBeUndefined(); + expect(result.status, result.stderr).toBe(1); + expect(result.stderr).toContain("generation failed:"); + expect(result.stderr).toContain("missing-schema.json"); + } + ); + it("selects checked-out schemas for normal nested generation with a clean environment", async () => { const root = await mkdtemp(join(tmpdir(), "copilot-nested-codegen-")); onTestFinished(() => rm(root, { recursive: true, force: true })); diff --git a/nodejs/test/testBackend.test.ts b/nodejs/test/testBackend.test.ts new file mode 100644 index 0000000000..94f74aaccf --- /dev/null +++ b/nodejs/test/testBackend.test.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +import type { ResumeSessionConfig, SessionConfig } from "../src"; +import { parseTestBackend, withTestBackend } from "./e2e/harness/testBackend"; + +describe("E2E backend selection", () => { + it.each([ + [undefined, "capi"], + ["", "capi"], + ["capi", "capi"], + [" ANTHROPIC-MESSAGES ", "anthropic-messages"], + ["openai-responses", "openai-responses"], + ["openai-completions", "openai-completions"], + ])("parses %s as %s", (value, expected) => { + expect(parseTestBackend(value)).toBe(expected); + }); + + it("rejects an unknown backend rather than running CAPI", () => { + expect(() => parseTestBackend("openai-response")).toThrow( + "Unsupported COPILOT_SDK_E2E_BACKEND" + ); + }); + + it.each([ + ["anthropic-messages", "anthropic", undefined, "claude-sonnet-5"], + ["openai-responses", "openai", "responses", "gpt-4.1"], + ["openai-completions", "openai", "completions", "gpt-4.1"], + ] as const)("configures %s for create and resume", (backend, type, wireApi, model) => { + const create: SessionConfig = { sessionId: "new-session" }; + const resume: ResumeSessionConfig = { disableResume: true }; + for (const config of [create, resume]) { + expect(withTestBackend(config, backend, "http://localhost:1234")).toEqual({ + ...config, + model, + provider: { + baseUrl: "http://localhost:1234", + type, + wireApi, + bearerToken: "fake-byok-credential-for-e2e-tests", + modelId: model, + wireModel: model, + }, + }); + expect(config).not.toHaveProperty("provider"); + } + }); + + it("preserves explicit models and provider configurations", () => { + const explicitModel: SessionConfig = { model: "test-model" }; + expect( + withTestBackend(explicitModel, "openai-responses", "http://localhost:1234") + ).toMatchObject({ + model: "test-model", + provider: { modelId: "test-model", wireModel: "test-model" }, + }); + const configs: SessionConfig[] = [ + { provider: { type: "openai", baseUrl: "https://example.com" } }, + { providers: [] }, + ]; + for (const config of configs) { + expect(withTestBackend(config, "openai-responses", "http://localhost:1234")).toBe( + config + ); + } + }); + + it("leaves CAPI configurations unchanged", () => { + const config: SessionConfig = { model: "test-model" }; + expect(withTestBackend(config, "capi", "http://localhost:1234")).toBe(config); + }); +}); diff --git a/nodejs/test/typescript-codegen.test.ts b/nodejs/test/typescript-codegen.test.ts index 77009e90a8..1ff222dd19 100644 --- a/nodejs/test/typescript-codegen.test.ts +++ b/nodejs/test/typescript-codegen.test.ts @@ -1,19 +1,56 @@ import type { JSONSchema7 } from "json-schema"; import { compile } from "json-schema-to-typescript"; -import { describe, expect, it } from "vitest"; +import { describe, expect, expectTypeOf, it } from "vitest"; import { assertNoPublicInternalReferences, filterPublicSessionEventVariants, - isTypeScriptCodegenEntrypoint, normalizeSchemaForTypeScript, + tsNullableResultTypeName, } from "../../scripts/codegen/typescript.ts"; -import type { DefinitionCollections } from "../../scripts/codegen/utils.ts"; +import type { AssistantMessageReasoningBlocks } from "../src/generated/session-events.ts"; +import { isCodegenEntrypoint, type DefinitionCollections } from "../../scripts/codegen/utils.ts"; describe("typescript schema codegen", () => { + it("preserves an explicit nullable reference result's named union", () => { + expect( + tsNullableResultTypeName( + { + rpcMethod: "session.accounts.getCurrent", + params: null, + result: { $ref: "#/definitions/SessionAccountResult" }, + }, + { + title: "SessionAccountResult", + anyOf: [{ $ref: "#/definitions/SessionAccount" }, { type: "null" }], + } + ) + ).toBe("SessionAccountResult"); + }); + + it("retains the existing undefined result type for omission sentinels", () => { + expect( + tsNullableResultTypeName({ + rpcMethod: "session.accounts.getCurrent", + params: null, + result: { + anyOf: [{ $ref: "#/definitions/SessionAccount" }, { not: {} }], + }, + }) + ).toBe("SessionAccount | undefined"); + }); + + it("keeps reasoning-block SDK types permissive like the other language projections", () => { + expectTypeOf<{ + provider: string; + blocks: []; + orderedBlocks: []; + }>().toExtend(); + }); + it("recognizes Windows entrypoint paths case-insensitively", () => { expect( - isTypeScriptCodegenEntrypoint( + isCodegenEntrypoint( "C:\\b\\execroot\\src\\sdk\\scripts\\codegen\\typescript.ts", "c:\\B\\execroot\\src\\sdk\\scripts\\codegen\\typescript.ts", "win32" @@ -60,6 +97,40 @@ describe("typescript schema codegen", () => { expect(code).toContain('inlineMode: /** Use a direct value. */ "direct" | "indirect";'); }); + it("accepts future reload values without widening other enum definitions", async () => { + const enumDefinition = (value: string): JSONSchema7 => ({ + type: "string", + enum: [value], + "x-enumDescriptions": { [value]: `Known ${value} value.` }, + }); + const code = await compile( + normalizeSchemaForTypeScript({ + title: "ReloadOptions", + type: "object", + properties: { + status: { $ref: "#/definitions/CustomizationReloadStatus" }, + subsystem: { $ref: "#/definitions/CustomizationReloadSubsystem" }, + other: { $ref: "#/definitions/OtherStatus" }, + }, + definitions: { + CustomizationReloadStatus: enumDefinition("reloaded"), + CustomizationReloadSubsystem: enumDefinition("skills"), + OtherStatus: enumDefinition("known"), + }, + }), + "ReloadOptions", + { bannerComment: "", style: { semi: true, singleQuote: false } } + ); + + expect(code).toMatch( + /export type CustomizationReloadStatus\s*=\s*\/\*\* Known reloaded value\. \*\/ "reloaded" \| \(string & \{\}\);/ + ); + expect(code).toMatch( + /export type CustomizationReloadSubsystem\s*=\s*\/\*\* Known skills value\. \*\/ "skills" \| \(string & \{\}\);/ + ); + expect(code).toContain('export type OtherStatus = /** Known known value. */ "known";'); + }); + it("maps bare opaque properties to their marker aliases", async () => { const code = await compile( normalizeSchemaForTypeScript({ diff --git a/nodejs/test/unified-release.test.ts b/nodejs/test/unified-release.test.ts new file mode 100644 index 0000000000..0463b2d201 --- /dev/null +++ b/nodejs/test/unified-release.test.ts @@ -0,0 +1,1106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { execFileSync } from "node:child_process"; +import { join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { c as createTar } from "tar"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { getTaskSteps } from "../../scripts/run-tasks.mjs"; +import { RUNTIME_PLATFORMS } from "../src/runtimeArtifacts.js"; +import { + copyPackages, + packageRelease, + pythonPackageVersion, + stampRustPackageManifest, + writeRustReleaseSnapshots, +} from "../scripts/package-release.js"; +import { + previousSdkReleaseTag, + publishJava, + publishRust, + publishSourceRelease, + recordRuntimeSdkVersion, +} from "../scripts/publish-release.js"; +import { + checksums, + checkReleaseSources, + assemblePackages, + packageMatrix, + packagedSdks, + planSdkVersion, + planGitUnstableSdkVersion, + publicationSdks, + rustReleaseLocation, + sealPackages, + seedRustRuntime, + selectedSdks, + sha256, + stageRuntime, + verifyPackages, + type ReleaseIdentity, +} from "../scripts/unified-release.js"; + +const roots: string[] = []; +const sourceSha = "a".repeat(40); +const identity: ReleaseIdentity = { + sourceSha, + runtimeVersion: "1.0.89-2", + sdkVersion: "0.4.0-preview.1", + channel: "prerelease", + visibility: "public", +}; +const plan = { + sourceSha, + channel: "latest" as const, + visibility: "public" as const, + runId: "123", + versions: { latest: "0.3.0", prerelease: "0.4.0-preview.0" }, +}; + +function fixture(): string { + const root = mkdtempSync(join(process.cwd(), ".sdk-release-test-")); + roots.push(root); + return root; +} + +function write(path: string, text: string): void { + mkdirSync(resolve(path, ".."), { recursive: true }); + writeFileSync(path, text); +} + +afterEach(() => { + vi.unstubAllEnvs(); + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +describe("release helper command contracts", () => { + it.each([ + ["unstable", "public", [["nodejs", "python", "go", "dotnet", "java", "rust"]]], + ["unstable", "internal", [["nodejs"]]], + ["prerelease", "public", [["nodejs", "python", "go", "dotnet", "java", "rust"]]], + ["latest", "public", [["nodejs", "python", "go", "dotnet", "java", "rust"]]], + ] as const)( + "dispatches valid freshness tasks for %s/%s", + (channel, visibility, expectedLanguages) => { + const product = fixture(); + execFileSync("git", ["init", "--quiet"], { cwd: product, windowsHide: true }); + const generation: { task: string; languages: string[] }[] = []; + const protocolCommands: string[][] = []; + + // Use the real task registry; only the compiler/code-generator execution is replaced. + checkReleaseSources(product, channel, visibility, (args) => { + if (args[0] === "src/sdk/scripts/run-tasks.mjs") { + expect(args.slice(-2)).toEqual(["--runtime-source", "checkout"]); + const language = args[2]?.startsWith("--") ? undefined : args[2]; + const steps = getTaskSteps(args[1], language); + generation.push({ + task: args[1], + languages: steps.map((step) => step.language), + }); + } else { + protocolCommands.push(args); + } + }); + + expect(generation).toEqual([ + { task: "generate:schemas", languages: [] }, + ...expectedLanguages.map((languages) => ({ task: "generate", languages })), + ]); + expect(protocolCommands).toEqual([ + ["src/sdk/nodejs/scripts/update-protocol-version.ts"], + ]); + } + ); + + it.each(["unified-release.ts", "package-release.ts", "publish-release.ts"])( + "starts the %s CLI without publication", + (script) => { + const directory = fixture(); + const loader = pathToFileURL( + resolve(import.meta.dirname, "../node_modules/tsx/dist/loader.mjs") + ).href; + const output = execFileSync( + process.execPath, + ["--import", loader, resolve(import.meta.dirname, "../scripts", script), "--help"], + { cwd: directory, encoding: "utf8", windowsHide: true } + ); + expect(output).toMatch(/^Usage:/); + expect(readdirSync(directory)).toEqual([]); + } + ); +}); + +describe("matched SDK release planning", () => { + it("supports direct stable and compatible promotion versions using the existing calculator", () => { + expect(planSdkVersion({ ...plan, versions: { latest: "0.3.0" } })).toBe("0.3.1"); + expect( + planSdkVersion({ ...plan, versions: { latest: "0.3.0", prerelease: undefined } }) + ).toBe("0.3.1"); + expect(planSdkVersion(plan)).toBe("0.4.0"); + expect(planSdkVersion({ ...plan, channel: "prerelease" })).toBe("0.4.0-preview.1"); + expect(planSdkVersion({ ...plan, override: "2.0.0" })).toBe("2.0.0"); + expect(planSdkVersion({ ...plan, override: "0.3.1" })).toBe("0.3.1"); + expect(planSdkVersion({ ...plan, channel: "prerelease", override: "0.5.0-pre.2" })).toBe( + "0.5.0-pre.2" + ); + }); + + it("never derives an unstable version from npm baselines", () => { + expect(() => planSdkVersion({ ...plan, channel: "unstable" })).toThrow( + "require source-history planning" + ); + }); + + describe("runtime-repository SDK version tags", () => { + function history() { + const root = fixture(); + const git = (args: string[], date = "2026-01-01T12:00:00Z") => + execFileSync("git", args, { + cwd: root, + encoding: "utf8", + env: { + ...process.env, + GIT_AUTHOR_DATE: date, + GIT_COMMITTER_DATE: date, + }, + }).trim(); + git(["init", "--quiet"]); + git(["config", "user.name", "release-test"]); + git(["config", "user.email", "release-test@example.invalid"]); + git(["config", "commit.gpgsign", "false"]); + git(["config", "tag.gpgsign", "false"]); + git(["commit", "--quiet", "--allow-empty", "-m", "SDK source"]); + return { root, git, sha: git(["rev-parse", "HEAD"]) }; + } + + it("uses nearest eligible first-parent tags, highest version ties, and tag publication time", () => { + const { root, git, sha: base } = history(); + git(["tag", "-a", "sdk-0.3.0", "-m", "Released"]); + git(["commit", "--quiet", "--allow-empty", "-m", "Next source"]); + git(["tag", "-a", "sdk-0.4.0-preview.2", "-m", "Preview"], "2026-01-02T00:00:00Z"); + git( + ["tag", "-a", "sdk-0.5.0-preview.1", "-m", "Higher preview"], + "2026-01-03T00:00:00Z" + ); + git(["commit", "--quiet", "--allow-empty", "-m", "Selected source"]); + const selected = git(["rev-parse", "HEAD"]); + git(["tag", "-a", "sdk-0.6.0", "-m", "Later publication"], "2026-02-01T00:00:00Z"); + git(["checkout", "--quiet", "--detach", base]); + git(["commit", "--quiet", "--allow-empty", "-m", "Other history"]); + git(["tag", "-a", "sdk-9.0.0", "-m", "Not an ancestor"]); + const version = planGitUnstableSdkVersion( + root, + selected, + "2026-01-05T00:00:00Z", + "123" + ); + expect(version).toBe(`0.5.0-unstable.123.g${selected.slice(0, 7)}`); + expect(planGitUnstableSdkVersion(root, selected, "2026-02-02T00:00:00Z", "124")).toBe( + `0.6.1-unstable.124.g${selected.slice(0, 7)}` + ); + expect(planGitUnstableSdkVersion(root, base, "2026-01-05T00:00:00Z", "125")).toBe( + `0.3.1-unstable.125.g${base.slice(0, 7)}` + ); + }); + + it("uses a source-anchored bootstrap until an eligible SDK tag exists", () => { + const { root, git, sha } = history(); + const bootstrap = { + version: "0.3.0", + sourceSha: sha, + publishedAt: "2026-01-02T00:00:00Z", + }; + git(["commit", "--quiet", "--allow-empty", "-m", "Selected source"]); + const selected = git(["rev-parse", "HEAD"]); + const version = () => + planGitUnstableSdkVersion(root, selected, "2026-01-05T00:00:00Z", "123", bootstrap); + expect(version()).toBe(`0.3.1-unstable.123.g${selected.slice(0, 7)}`); + git(["tag", "-a", "sdk-9.0.0", "-m", "Future"], "2026-02-01T00:00:00Z"); + expect(version()).toBe(`0.3.1-unstable.123.g${selected.slice(0, 7)}`); + git(["tag", "-a", "sdk-0.4.0", "-m", "Released"], "2026-01-03T00:00:00Z"); + expect(version()).toBe(`0.4.1-unstable.123.g${selected.slice(0, 7)}`); + }); + + it("does not apply the bootstrap before publication or outside its source history", () => { + const { root, git, sha } = history(); + const bootstrap = { + version: "0.3.0", + sourceSha: sha, + publishedAt: "2026-01-02T00:00:00Z", + }; + expect(() => + planGitUnstableSdkVersion(root, sha, "2026-01-01T00:00:00Z", "123", bootstrap) + ).toThrow("No eligible"); + git(["commit", "--quiet", "--allow-empty", "-m", "Later baseline"]); + expect(() => + planGitUnstableSdkVersion(root, sha, "2026-01-05T00:00:00Z", "123", { + ...bootstrap, + sourceSha: git(["rev-parse", "HEAD"]), + }) + ).toThrow("No eligible"); + }); + + it("rejects absent, unpublished-at-start, and lightweight SDK baselines", () => { + const { root, git, sha } = history(); + expect(() => + planGitUnstableSdkVersion(root, sha, "2026-01-05T00:00:00Z", "123") + ).toThrow("No eligible"); + git(["tag", "-a", "sdk-0.4.0", "-m", "Future"], "2026-02-01T00:00:00Z"); + expect(() => + planGitUnstableSdkVersion(root, sha, "2026-01-05T00:00:00Z", "123") + ).toThrow("No eligible"); + git(["tag", "sdk-0.3.0"]); + expect(() => + planGitUnstableSdkVersion(root, sha, "2026-01-05T00:00:00Z", "123") + ).toThrow("must be an annotated"); + }); + + it("plans an internal unstable release through the actual CLI without npm metadata", () => { + const { root, git, sha } = history(); + git(["tag", "-a", "sdk-0.3.0", "-m", "Verified baseline"]); + const output = join(root, "plan.json"); + const loader = pathToFileURL(resolve("node_modules/tsx/dist/loader.mjs")).href; + execFileSync( + process.execPath, + ["--import", loader, resolve("scripts/unified-release.ts"), "plan", output, root], + { + encoding: "utf8", + env: { + ...process.env, + SOURCE_SHA: sha, + RUNTIME_VERSION: "1.0.89-unstable.r123.gabcdef0", + CHANNEL: "unstable", + VISIBILITY: "internal", + SDK_VERSION: "", + GITHUB_RUN_ID: "123", + WORKFLOW_CREATED_AT: "2026-01-05T00:00:00Z", + // Any accidental npm baseline request fails instead of reaching the registry. + npm_config_offline: "true", + npm_config_cache: join(root, "empty-npm-cache"), + }, + } + ); + expect(JSON.parse(readFileSync(output, "utf8")).sdkVersion).toBe( + `0.3.1-unstable.123.g${sha.slice(0, 7)}` + ); + }); + + it("records annotated source tags once and preserves their timestamp on retry", () => { + const calls: string[][] = []; + const tagSha = "b".repeat(40); + let created = false; + const run = (args: string[]) => { + calls.push(args); + if (args[1]?.endsWith(`/git/ref/tags/sdk-${identity.sdkVersion}`)) { + if (created) return JSON.stringify({ object: { type: "tag", sha: tagSha } }); + throw Object.assign(new Error("missing"), { stderr: "(HTTP 404)" }); + } + if (args[1]?.endsWith(`/git/tags/${tagSha}`)) + return JSON.stringify({ + tag: `sdk-${identity.sdkVersion}`, + object: { type: "commit", sha: identity.sourceSha }, + tagger: { date: "2026-01-01T00:00:00Z" }, + }); + if (args[3]?.endsWith("/git/tags")) return JSON.stringify({ sha: tagSha }); + if (args[3]?.endsWith("/git/refs")) { + created = true; + return "{}"; + } + throw new Error(`Unexpected Git request: ${args}`); + }; + recordRuntimeSdkVersion(identity, run); + const writes = calls.filter((args) => args.includes("POST")); + expect(writes).toHaveLength(2); + expect(writes[0]).toContain(`object=${identity.sourceSha}`); + expect(writes[1]).toContain(`sha=${tagSha}`); + recordRuntimeSdkVersion(identity, run); + expect(calls.filter((args) => args.includes("POST"))).toHaveLength(2); + }); + + it.each(["lightweight", "conflicting"])("rejects an existing %s source tag", (kind) => { + const run = (args: string[]) => + args[1].includes("/git/ref/") + ? JSON.stringify({ + object: { + type: kind === "lightweight" ? "commit" : "tag", + sha: "b".repeat(40), + }, + }) + : JSON.stringify({ + tag: `sdk-${identity.sdkVersion}`, + object: { type: "commit", sha: "c".repeat(40) }, + tagger: { date: "2026-01-01T00:00:00Z" }, + }); + expect(() => recordRuntimeSdkVersion(identity, run)).toThrow( + kind === "lightweight" ? "annotated" : "another source" + ); + }); + }); + + it("rejects invalid channel/visibility/version combinations before packaging", () => { + expect(selectedSdks("latest", "public")).toEqual([ + "nodejs", + "python", + "go", + "dotnet", + "rust", + "java", + ]); + expect(selectedSdks("unstable", "public")).toEqual(selectedSdks("latest", "public")); + expect(publicationSdks("unstable", "public", "internal")).toEqual(["nodejs"]); + expect(publicationSdks("latest", "public", "internal")).toEqual(["nodejs", "dotnet"]); + expect(publicationSdks("unstable", "public", "public")).toEqual([ + "nodejs", + "dotnet", + "rust", + "java", + ]); + expect(packagedSdks("unstable", "public")).toContain("python"); + expect(selectedSdks("unstable", "internal")).toEqual(["nodejs"]); + expect(() => selectedSdks("latest", "internal")).toThrow("must be unstable"); + expect(() => selectedSdks("canary", "public")).toThrow("Invalid SDK channel"); + expect(() => planSdkVersion({ ...plan, override: "0.5.0-preview.1" })).toThrow("channel"); + expect( + planSdkVersion({ ...plan, channel: "prerelease", override: "0.5.0-chicken.1" }) + ).toBe("0.5.0-chicken.1"); + }); +}); + +describe("same-run SDK artifacts", () => { + it("keeps Go as a source release without a packaging job or archive", async () => { + for (const [key, value] of Object.entries({ + SOURCE_SHA: identity.sourceSha, + RUNTIME_VERSION: identity.runtimeVersion, + SDK_VERSION: identity.sdkVersion, + CHANNEL: identity.channel, + VISIBILITY: identity.visibility, + })) + vi.stubEnv(key, value); + expect(selectedSdks(identity.channel, identity.visibility)).toContain("go"); + await expect(packageRelease("go", "", "", "", "")).rejects.toThrow("not selected"); + }); + + it.each([ + ["python", "prerelease"], + ["python", "unstable"], + ["dotnet", "prerelease"], + ["dotnet", "unstable"], + ["rust", "prerelease"], + ] as const)( + "packages %s/%s without running a consumer application", + async (language, channel) => { + vi.stubEnv("COPILOT_SKIP_CLI_DOWNLOAD", undefined); + vi.stubEnv("DOCS_RS", undefined); + const root = fixture(); + const product = join(root, "product"); + const runtime = join(root, "runtime"); + const output = join(root, "packages"); + mkdirSync(product); + execFileSync("git", ["init", "--quiet"], { cwd: product }); + execFileSync( + "git", + [ + "-c", + "user.name=release-test", + "-c", + "user.email=release-test@example.invalid", + "commit", + "--quiet", + "--allow-empty", + "-m", + "Selected product", + ], + { cwd: product } + ); + const sha = execFileSync("git", ["rev-parse", "HEAD"], { + cwd: product, + encoding: "utf8", + }).trim(); + const release = { + ...identity, + sourceSha: sha, + channel, + sdkVersion: + channel === "unstable" + ? `0.4.0-unstable.123.g${sha.slice(0, 7)}` + : identity.sdkVersion, + runtimeVersion: + channel === "unstable" + ? `1.0.89-2.unstable.r123.g${sha.slice(0, 7)}` + : identity.runtimeVersion, + }; + for (const [key, value] of Object.entries({ + SOURCE_SHA: sha, + RUNTIME_VERSION: release.runtimeVersion, + SDK_VERSION: release.sdkVersion, + CHANNEL: release.channel, + VISIBILITY: release.visibility, + })) + vi.stubEnv(key, value); + for (const schema of ["api.schema.json", "session-events.schema.json"]) { + write(join(product, "generated", schema), '{"same":"source"}'); + for (const platform of RUNTIME_PLATFORMS) { + write(join(runtime, platform, "schemas", schema), '{"same":"source"}'); + } + } + const commands: string[] = []; + if (language === "python") { + write( + join(product, "src", "sdk", "python", "pyproject.toml"), + 'version = "0.0.0"\n' + ); + } + const rust = join(product, "src", "sdk", "rust"); + const bundle = join(root, "bundle"); + if (language === "rust") { + write( + join(rust, "Cargo.toml"), + '[package]\nname = "github-copilot-sdk"\nversion = "0.0.0-dev"\n' + ); + write(join(rust, "Cargo.lock"), "original dependency resolutions"); + const assets = [ + ["dist-bin", "copilot-linux-x64.tar.gz"], + [ + "dist-pkg-tarballs", + `github-copilot-${identity.runtimeVersion}-linux-x64.tgz`, + ], + ].map(([directory, name]) => { + const file = join(bundle, "github-release", directory, name); + write(file, "runtime bytes"); + return `${sha256(file)} ${name}`; + }); + write( + join(bundle, "github-release", "dist-bin", "SHA256SUMS.txt"), + assets.join("\n") + ); + } + await packageRelease(language, product, bundle, runtime, output, (command, args) => { + commands.push(command === process.execPath ? args[0] : command); + if (command === "tar") write(args[1], "selected Rust source"); + if (command === "uv") { + expect(args.slice(0, 3)).toEqual(["build", "--wheel", "--out-dir"]); + expect( + readFileSync( + join(product, "src", "sdk", "python", "pyproject.toml"), + "utf8" + ) + ).toBe( + `version = "${channel === "unstable" ? "0.4.0.dev123" : identity.sdkVersion}"\n` + ); + write(join(args[3], "sdk.whl"), "packaged Python SDK"); + } + if (command === "dotnet") { + expect(args[0]).toBe("pack"); + expect(args).toContain(`-p:Version=${release.sdkVersion}`); + expect(args).toContain(`-p:CopilotCliVersion=${release.runtimeVersion}`); + const destination = args.find((arg) => arg.startsWith("-p:PackageOutputPath=")); + expect(destination).toBe(`-p:PackageOutputPath=${join(output, "dotnet")}`); + write(join(output, "dotnet", "sdk.nupkg"), "packaged .NET SDK"); + } + if (command === "cargo") { + expect(readFileSync(join(rust, "Cargo.toml"), "utf8")).toContain( + `version = "${identity.sdkVersion}"` + ); + if (args[0] === "update") { + expect(args).toEqual(["update", "--workspace"]); + write( + join(rust, "Cargo.lock"), + "updated workspace version; original dependency resolutions" + ); + } else { + expect(args).toEqual(["package", "--locked", "--allow-dirty"]); + expect(readFileSync(join(rust, "Cargo.lock"), "utf8")).toBe( + "updated workspace version; original dependency resolutions" + ); + write(join(rust, "target", "package", "sdk.crate"), "packaged Rust SDK"); + } + } + }); + expect(commands).toEqual([ + "scripts/set-cli-version.js", + "scripts/set-version.js", + ...(language === "python" + ? ["scripts/inject-cli-version.mjs", "uv"] + : language === "rust" + ? ["bash", "bash", "cargo", "cargo", "tar"] + : ["dotnet"]), + ]); + expect(() => verifyPackages(output, release, language)).not.toThrow(); + expect(readdirSync(output).sort()).toEqual([language, "release-manifest.json"].sort()); + if (language !== "rust") { + vi.stubEnv("CHANNEL", "unstable"); + vi.stubEnv("SDK_VERSION", "0.4.0-unstable.123"); + vi.stubEnv("VISIBILITY", "internal"); + await expect( + packageRelease(language, product, "", "", "", () => { + throw new Error("Unselected language must not execute"); + }) + ).rejects.toThrow("not selected"); + } + } + ); + + it("maps unstable Python versions to unique ordered PyPI development releases", () => { + expect(pythonPackageVersion(identity)).toBe(identity.sdkVersion); + expect(pythonPackageVersion({ ...identity, channel: "latest", sdkVersion: "1.0.15" })).toBe( + "1.0.15" + ); + for (const run of ["123", "36638597907"]) { + expect( + pythonPackageVersion({ + ...identity, + channel: "unstable", + sdkVersion: `1.0.15-unstable.${run}.gabcdef0`, + }) + ).toBe(`1.0.15.dev${run}`); + } + expect(() => + pythonPackageVersion({ + ...identity, + channel: "unstable", + sdkVersion: "1.0.15-unstable", + }) + ).toThrow("run ID and source SHA"); + }); + + it("keeps a staged Rust SDK independent of ancestor runtime workspaces", () => { + const manifest = + '[package]\nname = "github-copilot-sdk"\nversion = "0.0.0-dev"\nedition = "2024"\n'; + const stamped = stampRustPackageManifest(manifest, "1.0.15-preview.3"); + expect(stamped).toBe( + '[package]\nname = "github-copilot-sdk"\nversion = "1.0.15-preview.3"\nedition = "2024"\n\n[workspace]\n' + ); + expect(stampRustPackageManifest(stamped, "1.0.15-preview.3")).toBe(stamped); + const existing = `${manifest}\n[workspace.package]\nlicense = "MIT"\n`; + expect(stampRustPackageManifest(existing, "1.0.15-preview.3")).toBe( + existing.replace('"0.0.0-dev"', '"1.0.15-preview.3"') + ); + }); + + describe("parallel SDK artifact assembly", () => { + function part(parts: string, language: string, release = identity): string { + const directory = join(parts, `sdk-package-${language}`); + write(join(directory, language, "package.bin"), `${language} bytes`); + sealPackages(directory, release, language); + return directory; + } + + it.each([ + ["latest", "public", ["nodejs", "python", "dotnet", "rust", "java"]], + ["prerelease", "public", ["nodejs", "python", "dotnet", "rust", "java"]], + ["unstable", "public", ["nodejs", "python", "dotnet", "rust", "java"]], + ["unstable", "internal", ["nodejs"]], + ] as const)( + "assembles exactly the %s/%s language matrix", + (channel, visibility, languages) => { + const release = { + ...identity, + channel, + visibility, + sdkVersion: + channel === "latest" + ? "0.4.0" + : channel === "unstable" + ? "0.4.0-unstable.123" + : identity.sdkVersion, + }; + const root = fixture(); + const parts = join(root, "parts"); + const output = join(root, "release"); + const matrix = packageMatrix(channel, visibility); + expect(matrix.include.map((entry) => entry.language)).toEqual(languages); + expect(selectedSdks(channel, visibility).includes("go")).toBe( + visibility === "public" + ); + expect(matrix.include.every((entry) => entry.name.length > 0)).toBe(true); + for (const language of languages) part(parts, language, release); + assemblePackages(parts, output, release); + expect(() => verifyPackages(output, release)).not.toThrow(); + expect(existsSync(join(output, "go"))).toBe(false); + for (const language of languages) { + expect(readFileSync(join(output, language, "package.bin"), "utf8")).toBe( + `${language} bytes` + ); + } + expect( + JSON.parse(readFileSync(join(output, "release-manifest.json"), "utf8")).language + ).toBeUndefined(); + } + ); + + it.each(["missing", "extra", "identity", "tampered", "wrong-language"])( + "rejects %s language artifacts before producing an assembled release", + (failure) => { + const root = fixture(); + const parts = join(root, "parts"); + const output = join(root, "release"); + for (const language of packagedSdks(identity.channel, identity.visibility)) { + if (failure === "missing" && language === "java") continue; + part(parts, language); + } + if (failure === "extra") mkdirSync(join(parts, "sdk-package-unexpected")); + const node = join(parts, "sdk-package-nodejs"); + if (failure === "identity") + sealPackages(node, { ...identity, sdkVersion: "0.4.0-preview.2" }, "nodejs"); + if (failure === "tampered") + write(join(node, "nodejs", "package.bin"), "modified package"); + if (failure === "wrong-language") { + const manifestPath = join(node, "release-manifest.json"); + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + manifest.language = "rust"; + write(manifestPath, JSON.stringify(manifest)); + } + expect(() => assemblePackages(parts, output, identity)).toThrow(); + expect(existsSync(output)).toBe(false); + } + ); + + it("rejects artifacts for a language outside its matrix job's scope", () => { + const root = fixture(); + write(join(root, "nodejs", "package.tgz"), "node"); + write(join(root, "rust", "package.crate"), "rust"); + expect(() => sealPackages(root, identity, "nodejs")).toThrow("Unexpected SDK artifact"); + }); + }); + + it("retains the Python wheel rather than uv output-directory metadata", () => { + const root = fixture(); + const output = join(root, "uv-output"); + const retained = join(root, "retained"); + const wheel = "github_copilot_sdk-1.0.15rc3-py3-none-any.whl"; + write(join(output, ".gitignore"), "*"); + write(join(output, wheel), "built wheel"); + write(join(output, "build.log"), "auxiliary output"); + mkdirSync(retained); + expect(copyPackages(output, retained, [".whl"])).toEqual([join(retained, wheel)]); + expect(readdirSync(retained)).toEqual([wheel]); + expect(readFileSync(join(retained, wheel), "utf8")).toBe("built wheel"); + }); + + it("accepts unstable packaging only when the selected Rust implementation supports its release location", () => { + const rust = join(fixture(), "unstable-rust"); + const implementation = + '"release-url"\nhttps://github.com/github/copilot-sdk/releases/download/runtime-'; + write(join(rust, "build", "in_process.rs"), implementation); + const commands: string[][] = []; + expect(() => + writeRustReleaseSnapshots( + rust, + "bundle", + { + ...identity, + channel: "unstable", + sdkVersion: "0.4.0-unstable.123", + }, + (args) => commands.push(args) + ) + ).not.toThrow(); + expect(commands).toHaveLength(2); + expect(commands[0]).toContain( + `https://github.com/github/copilot-sdk/releases/download/runtime-${identity.runtimeVersion}` + ); + expect(readFileSync(join(rust, "build", "in_process.rs"), "utf8")).toBe(implementation); + }); + + it.each(["latest", "prerelease"] as const)( + "uses reviewed snapshot producers for compatible old %s source without changing Rust code", + (channel) => { + const rust = join(fixture(), "old-rust"); + const implementation = "selected legacy Rust runtime implementation"; + write(join(rust, "build.rs"), "selected build entrypoint"); + write(join(rust, "build", "in_process.rs"), implementation); + write(join(rust, "Cargo.toml"), "selected manifest"); + const commands: string[][] = []; + const releaseIdentity = { + ...identity, + channel, + sdkVersion: channel === "latest" ? "0.4.0" : identity.sdkVersion, + }; + writeRustReleaseSnapshots(rust, "bundle", releaseIdentity, (args) => + commands.push(args) + ); + expect(commands).toHaveLength(2); + expect(commands.map((args) => args.at(-1))).toEqual([ + join(rust, "cli-version.txt"), + join(rust, "cli-version-in-process.txt"), + ]); + for (const args of commands) { + expect(args[0]).not.toContain(rust); + expect(args).toEqual( + expect.arrayContaining([ + "--version", + identity.runtimeVersion, + "--release-url", + `https://github.com/github/copilot-cli/releases/download/v${identity.runtimeVersion}`, + "--checksums", + join("bundle", "github-release", "dist-bin", "SHA256SUMS.txt"), + "--output", + ]) + ); + } + expect(existsSync(join(rust, "scripts"))).toBe(false); + expect(readFileSync(join(rust, "build.rs"), "utf8")).toBe("selected build entrypoint"); + expect(readFileSync(join(rust, "build", "in_process.rs"), "utf8")).toBe(implementation); + expect(readFileSync(join(rust, "Cargo.toml"), "utf8")).toBe("selected manifest"); + expect(() => + writeRustReleaseSnapshots( + rust, + "bundle", + { ...identity, channel: "unstable" }, + () => { + throw new Error("unsupported source must not run snapshot tools"); + } + ) + ).toThrow("Selected Rust source does not support SDK-hosted unstable"); + } + ); + + it("stages all verified platforms and rejects version, SHA, checksum and support-file mismatches", async () => { + const root = fixture(); + const bundle = join(root, "bundle"); + const packages = join(bundle, "github-release", "dist-pkg-tarballs"); + const binaries = join(bundle, "github-release", "dist-bin"); + mkdirSync(packages, { recursive: true }); + mkdirSync(binaries, { recursive: true }); + write( + join(bundle, "github-release", "release-identity.json"), + JSON.stringify({ ...identity, signed: true }) + ); + const sums: string[] = []; + for (const platform of RUNTIME_PLATFORMS) { + const input = join(root, "input", platform); + write( + join(input, "package", "package.json"), + JSON.stringify({ version: identity.runtimeVersion }) + ); + for (const file of [ + "LICENSE.md", + "copilot-sdk/extension.js", + "preloads/extension_bootstrap.mjs", + `prebuilds/${platform}/runtime.node`, + `prebuilds/${platform}/copilot-runtime${platform.startsWith("win32") ? ".exe" : ""}`, + ]) { + write(join(input, "package", file), "runtime"); + } + const name = `github-copilot-${identity.runtimeVersion}-${platform}.tgz`; + await createTar({ cwd: input, file: join(packages, name), gzip: true }, ["package"]); + sums.push(`${sha256(join(packages, name))} ${name}`); + const binary = `copilot-${platform}.${platform.startsWith("win32") ? "zip" : "tar.gz"}`; + write(join(binaries, binary), "signed executable archive"); + sums.push(`${sha256(join(binaries, binary))} ${binary}`); + } + write(join(binaries, "SHA256SUMS.txt"), `${sums.join("\n")}\n`); + const output = join(root, "runtime"); + await stageRuntime(bundle, output, identity); + expect( + readFileSync( + join(output, "linux-x64", "prebuilds", "linux-x64", "runtime.node"), + "utf8" + ) + ).toBe("runtime"); + const cache = join(root, "rust-cache"); + seedRustRuntime(bundle, cache, identity); + expect( + readFileSync( + join(cache, `v${identity.runtimeVersion}-copilot-linux-x64.tar.gz`), + "utf8" + ) + ).toBe("signed executable archive"); + const unstable = { + ...identity, + channel: "unstable" as const, + sdkVersion: "0.4.0-unstable.123.gaaaaaaa", + }; + seedRustRuntime(bundle, cache, unstable); + expect(rustReleaseLocation(unstable).url).toBe( + `https://github.com/github/copilot-sdk/releases/download/runtime-${identity.runtimeVersion}` + ); + expect( + readFileSync( + join( + cache, + `copilot-sdk-runtime-${identity.runtimeVersion}-copilot-linux-x64.tar.gz` + ), + "utf8" + ) + ).toBe("signed executable archive"); + expect(() => rustReleaseLocation({ ...unstable, visibility: "internal" })).toThrow( + "Private Rust" + ); + await expect( + stageRuntime(bundle, output, { ...identity, sourceSha: "b".repeat(40) }) + ).rejects.toThrow("different source"); + await expect( + stageRuntime(bundle, output, { ...identity, runtimeVersion: "9.0.0" }) + ).rejects.toThrow("different version"); + const linuxInput = join(root, "input", "linux-x64"); + const linuxAsset = `github-copilot-${identity.runtimeVersion}-linux-x64.tgz`; + const repackLinux = async () => { + await createTar({ cwd: linuxInput, file: join(packages, linuxAsset), gzip: true }, [ + "package", + ]); + sums[sums.findIndex((line) => line.endsWith(` ${linuxAsset}`))] = + `${sha256(join(packages, linuxAsset))} ${linuxAsset}`; + write(join(binaries, "SHA256SUMS.txt"), `${sums.join("\n")}\n`); + }; + write(join(linuxInput, "package", "package.json"), '{"version":"9.0.0"}'); + await repackLinux(); + await expect(stageRuntime(bundle, output, identity)).rejects.toThrow( + "Wrong runtime version" + ); + write( + join(linuxInput, "package", "package.json"), + JSON.stringify({ version: identity.runtimeVersion }) + ); + rmSync(join(linuxInput, "package", "preloads", "extension_bootstrap.mjs")); + await repackLinux(); + await expect(stageRuntime(bundle, output, identity)).rejects.toThrow( + "extension_bootstrap.mjs" + ); + write(join(packages, linuxAsset), "corrupt"); + await expect(stageRuntime(bundle, output, identity)).rejects.toThrow("Checksum mismatch"); + expect(() => checksums(`${sums[0]}\n${sums[0]}`)).toThrow("Duplicate checksum"); + }); + + it("binds retained packages to source, channel, visibility, version and exact bytes", () => { + const root = fixture(); + const internal = { + ...identity, + sdkVersion: "0.4.0-unstable.123.gaaaaaaa", + channel: "unstable" as const, + visibility: "internal" as const, + }; + write(join(root, "nodejs", "package.tgz"), "exact package"); + sealPackages(root, internal); + expect(() => verifyPackages(root, internal)).not.toThrow(); + expect(() => verifyPackages(root, { ...internal, visibility: "public" })).toThrow( + "visibility" + ); + expect(() => verifyPackages(root, { ...internal, sourceSha: "b".repeat(40) })).toThrow( + "sourceSha" + ); + write(join(root, "nodejs", "extra.tgz"), "unexpected"); + expect(() => verifyPackages(root, internal)).toThrow("Unexpected retained files"); + rmSync(join(root, "nodejs", "extra.tgz")); + write(join(root, "nodejs", "package.tgz"), "wrong package"); + expect(() => verifyPackages(root, internal)).toThrow("Retained"); + }); + + it.each([false, true])( + "uses native Cargo with immutable source, lockfile and runtime pins (wrong pin=%s)", + async (wrongPin) => { + const root = fixture(); + const source = join(root, "source"); + const retained = join(root, "retained"); + const product = join(root, "product"); + const archiveName = `github-copilot-sdk-${identity.sdkVersion}.crate`; + write(join(retained, archiveName), "verified crate"); + write( + join(source, "rust", "Cargo.toml"), + `[package]\nversion = "${identity.sdkVersion}"\n` + ); + write(join(source, "rust", "Cargo.lock"), "retained Cargo.lock"); + write( + join(source, "rust", "cli-version.txt"), + `version=${wrongPin ? "9.0.0" : identity.runtimeVersion}\n` + ); + write( + join(source, "rust", "cli-version-in-process.txt"), + `version=${identity.runtimeVersion}\n` + ); + await createTar({ cwd: source, file: join(retained, "source.tar.gz"), gzip: true }, [ + "rust", + ]); + const commands: string[][] = []; + const run = (command: string, args: string[], cwd: string) => { + commands.push([command, ...args]); + if (command === "git") return identity.sourceSha; + expect(readFileSync(join(cwd, "Cargo.lock"), "utf8")).toBe("retained Cargo.lock"); + expect(readFileSync(join(cwd, "cli-version.txt"), "utf8")).toBe( + `version=${identity.runtimeVersion}\n` + ); + expect(readFileSync(join(cwd, "cli-version-in-process.txt"), "utf8")).toBe( + `version=${identity.runtimeVersion}\n` + ); + return ""; + }; + const publication = publishRust(retained, identity, product, run); + if (wrongPin) await expect(publication).rejects.toThrow("runtime pin differs"); + else await expect(publication).resolves.toBeUndefined(); + expect(commands.filter((command) => command[1] === "publish")).toHaveLength( + wrongPin ? 0 : 1 + ); + expect(commands.some((command) => command[1] === "package")).toBe(false); + for (const command of commands.filter((command) => command[0] === "cargo")) { + expect(command).toEqual( + expect.arrayContaining(["--locked", "--allow-dirty", "--no-verify"]) + ); + } + await expect( + publishRust(retained, identity, product, () => "b".repeat(40)) + ).rejects.toThrow("selected product SHA"); + } + ); + + it("uses native Maven deploy with retained classifier inputs and the exact runtime pin", async () => { + const root = fixture(); + const source = join(root, "source"); + const retained = join(root, "retained"); + const product = join(root, "product"); + const runtime = join(retained, `github-copilot-${identity.runtimeVersion}-linux-x64.tgz`); + write(join(source, "java", "pom.xml"), "retained Maven source"); + write( + join(retained, "nodejs-package.json"), + JSON.stringify({ copilotCliVersion: identity.runtimeVersion }) + ); + write(join(product, "src", "sdk", "nodejs", "package.json"), "{}"); + write(runtime, "verified runtime archive"); + await createTar({ cwd: source, file: join(retained, "source.tar.gz"), gzip: true }, [ + "java", + ]); + const commands: string[][] = []; + const run = (command: string, args: string[], cwd: string, env?: NodeJS.ProcessEnv) => { + commands.push([command, ...args]); + if (command === "git") return identity.sourceSha; + expect(command).toBe("bash"); + expect(readFileSync(join(cwd, "pom.xml"), "utf8")).toBe("retained Maven source"); + expect(args).toEqual( + expect.arrayContaining([ + "mvnw", + "deploy", + "-Prelease", + `-Drevision=${identity.sdkVersion}`, + ]) + ); + expect(args.filter((arg) => arg.startsWith("-Dcopilot.native.external."))).toHaveLength( + 6 + ); + expect(env?.COPILOT_CLI_RELEASE_TARBALL).toBe(runtime); + expect(env?.COPILOT_CLI_RELEASE_SHA256).toBe(sha256(runtime)); + return ""; + }; + await publishJava(retained, identity, product, run); + expect(commands.filter((command) => command[0] === "bash")).toHaveLength(1); + write(join(retained, "nodejs-package.json"), '{"copilotCliVersion":"9.0.0"}'); + await expect(publishJava(retained, identity, product, run)).rejects.toThrow( + "runtime pin differs" + ); + expect(commands.filter((command) => command[0] === "bash")).toHaveLength(1); + }); + + it.each(["prerelease", "unstable"] as const)( + "creates immutable %s source tags and announces only normal releases", + async (channel) => { + const release = { + ...identity, + channel, + sdkVersion: + channel === "unstable" ? "0.4.0-unstable.123.gaaaaaaa" : identity.sdkVersion, + }; + const snapshot = "b".repeat(40); + const requests: string[][] = []; + const gh = (args: string[]) => { + requests.push(args); + if (args[0] === "api" && args[1].includes("/git/ref/")) { + throw Object.assign(new Error("not found"), { stderr: "(HTTP 404)" }); + } + if (args[0] === "api" && args[1].includes("/releases/tags/")) { + throw Object.assign(new Error("not found"), { stderr: "(HTTP 404)" }); + } + if (args.includes("--slurp")) { + return JSON.stringify([ + [ + { + tag_name: "v0.3.0", + draft: false, + prerelease: false, + published_at: "2026-09-20T00:00:00Z", + }, + ], + ]); + } + return "{}"; + }; + await publishSourceRelease(release, snapshot, gh); + const tags = requests.filter((args) => args.includes("POST")); + expect(tags).toHaveLength(4); + expect( + tags.some((args) => args.includes(`ref=refs/tags/go/v${release.sdkVersion}`)) + ).toBe(true); + expect(tags.every((args) => args.includes(`sha=${snapshot}`))).toBe(true); + expect(tags.flat()).not.toContain(`sha=${identity.sourceSha}`); + if (channel === "unstable") { + expect( + requests.every( + (args) => args[0] === "api" && args.some((arg) => arg.includes("/git/")) + ) + ).toBe(true); + } else { + expect(requests.at(-1)).toContain("--latest=false"); + expect(requests.at(-1)).toEqual( + expect.arrayContaining(["--generate-notes", "--notes-start-tag", "v0.3.0"]) + ); + } + await expect(publishSourceRelease(release, identity.sourceSha, gh)).rejects.toThrow( + "private runtime SHA" + ); + await expect( + publishSourceRelease(release, snapshot, () => + JSON.stringify({ object: { type: "commit", sha: "c".repeat(40) } }) + ) + ).rejects.toThrow("another source"); + if (channel === "unstable") { + await publishSourceRelease(release, snapshot, (args) => { + expect(args[0]).toBe("api"); + expect(args[1]).toContain("/git/ref/tags/"); + return JSON.stringify({ object: { type: "commit", sha: snapshot } }); + }); + expect(() => recordRuntimeSdkVersion(release, gh)).toThrow("Only normal public"); + } + } + ); + + it("keeps stable versus prerelease changelog baselines and excludes hosted runtime releases", () => { + const releases = [ + { + tag_name: "v0.3.0", + draft: false, + prerelease: false, + published_at: "2026-09-20T00:00:00Z", + }, + { + tag_name: "v0.4.0-preview.0", + draft: false, + prerelease: true, + published_at: "2026-09-21T00:00:00Z", + }, + { + tag_name: "runtime-1.0.89-unstable.1", + draft: false, + prerelease: true, + published_at: "2026-09-22T00:00:00Z", + }, + { + tag_name: "v0.5.0", + draft: true, + prerelease: false, + published_at: "2026-09-23T00:00:00Z", + }, + ]; + expect(previousSdkReleaseTag(releases, identity)).toBe("v0.4.0-preview.0"); + expect( + previousSdkReleaseTag(releases, { ...identity, channel: "latest", sdkVersion: "0.4.0" }) + ).toBe("v0.3.0"); + }); +}); diff --git a/nodejs/tsconfig.test.json b/nodejs/tsconfig.test.json index f3916c8264..6f8ef72510 100644 --- a/nodejs/tsconfig.test.json +++ b/nodejs/tsconfig.test.json @@ -9,6 +9,7 @@ "src/**/*", "test/dependency-policy.test.ts", "test/copilot-request-handler.test.ts", + "test/connectors.test.ts", "test/ffiRuntimeHost.test.ts", "test/session-event-types.test.ts", "test/session-config-types.test.ts", diff --git a/nodejs/vitest.config.ts b/nodejs/vitest.config.ts index 4ac56bb45e..10a9ac4515 100644 --- a/nodejs/vitest.config.ts +++ b/nodejs/vitest.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from "vitest/config"; +import { isByokBackend } from "./test/e2e/harness/testBackend"; const integrationTestTimeout = process.platform === "win32" ? 60000 : 30000; @@ -25,6 +26,8 @@ const inProcessBlockedE2E = [ "**/test/e2e/hooks.e2e.test.ts", "**/test/e2e/hooks_extended.e2e.test.ts", "**/test/e2e/mcp_and_agents.e2e.test.ts", + "**/test/e2e/mcp_instruction_recovery.e2e.test.ts", + "**/test/e2e/memory_retrieval.e2e.test.ts", "**/test/e2e/mode_empty.e2e.test.ts", "**/test/e2e/multi_turn.e2e.test.ts", "**/test/e2e/permissions.e2e.test.ts", @@ -51,8 +54,26 @@ const inProcessBlockedE2E = [ "**/test/e2e/tools.e2e.test.ts", ]; +// These suites own their provider or CAPI auth/request-handler setup. They run +// in the ordinary CAPI job, not as repetitions of the matrix-selected backend. +const selfConfiguredBackendE2E = [ + "**/test/e2e/byok_bearer_token_provider.e2e.test.ts", + "**/test/e2e/copilot_request_handler.e2e.test.ts", + "**/test/e2e/copilot_request_session_id.e2e.test.ts", + "**/test/e2e/disabled_mcp_servers.e2e.test.ts", + "**/test/e2e/mcp_cached_oauth_overlap.e2e.test.ts", + "**/test/e2e/mcp_catalog_recovery.e2e.test.ts", + "**/test/e2e/memory_retrieval.e2e.test.ts", + "**/test/e2e/mode_handlers.e2e.test.ts", + "**/test/e2e/multi_provider_registry.e2e.test.ts", + "**/test/e2e/per_session_auth.e2e.test.ts", + "**/test/e2e/provider_endpoint.e2e.test.ts", + "**/test/e2e/subagent_prompt_resume.e2e.test.ts", +]; + export default defineConfig({ test: { + ...(isByokBackend ? { include: ["test/e2e/*.e2e.test.ts"] } : {}), globals: true, environment: "node", testTimeout: integrationTestTimeout, @@ -60,6 +81,7 @@ export default defineConfig({ teardownTimeout: 10000, isolate: true, // Run each test file in isolation pool: "forks", // Use process forking for better isolation + globalSetup: ["./test/e2e/harness/globalSetup.ts"], // Exclude our ad-hoc test files that aren't vitest-based exclude: [ "**/node_modules/**", @@ -67,6 +89,7 @@ export default defineConfig({ "**/*.d.ts", "**/basic-test.ts", // Old manual test ...(isInProcessTransport ? inProcessBlockedE2E : []), + ...(isByokBackend ? selfConfiguredBackendE2E : []), ], }, }); diff --git a/package.json b/package.json index d7ad4de360..34d2bde533 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "scripts": { "build": "node scripts/run-tasks.mjs build", + "build:default": "node scripts/run-tasks.mjs build:default", "build:nodejs": "node scripts/run-tasks.mjs build nodejs", "build:python": "node scripts/run-tasks.mjs build python", "build:go": "node scripts/run-tasks.mjs build go", diff --git a/python/README.md b/python/README.md index 99a18a4785..04cefd38f4 100644 --- a/python/README.md +++ b/python/README.md @@ -74,6 +74,11 @@ python chat.py ## Quick Start For experimental in-process AHP hosting, use `client.start_ahp_host(AhpHostOptions(...))`. +Select `local_server=HostLocalServerOptions()` for a local listener, +`github_environment=HostGitHubEnvironmentOptions(name="My app", compute_id="stable-installation-id")` +for Mission Control/WPS, or both. Import the transport types from `copilot.rpc`. +At least one transport is required. GitHub-only hosts have no `url` or `token`; +`host.environment_id` exposes their Mission Control identity. See [runtime-supervised AHP hosting](../docs/runtime-supervised-host.md) for creation and resume callbacks, resident-session publication, ownership, and shared-snapshot E2Es. @@ -254,7 +259,7 @@ All options are kw-only parameters: - `installation_confirmation_handler` (InstallationConfirmationHandler | None): Experimental connection-global human review for `installations.confirm`. Receives the typed request and one cancellation signal, and returns an explicit decision. Does not enable installation capabilities. - `use_logged_in_user` (bool | None): Whether to use logged-in user for authentication (default: True, but False when `github_token` is provided). - `telemetry` (dict | None): OpenTelemetry configuration for the CLI process. Providing this enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. -- `session_fs` (dict | None): Connection-level session filesystem provider configuration. +- `session_fs` (dict | None): Connection-level session filesystem provider configuration. To let `view` read provider-only images, set `capabilities.binary` to `True` and return a provider implementing `SessionFsBinaryProvider` (`read_file_bytes(path)` and `write_file_bytes(path, content, mode=None)`) from `create_session_fs_handler`; 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. - `session_idle_timeout_seconds` (int | None): Server-wide session idle timeout in seconds. Set to `None` or `0` to disable. - `enable_remote_sessions` (bool): Enable remote/cloud session support (default: False). - `on_list_models` (callable | None): Custom handler for `list_models()`. When provided, the handler is called instead of querying the runtime. @@ -513,6 +518,27 @@ tool = define_tool( ) ``` +#### Changing Tools on a Live Session (experimental) + +Use `await session.set_tools(tools)` to replace the complete set of custom tools +supplied by this SDK client connection. The method accepts the same `Tool` +definitions used with `create_session()` and `resume_session()`; built-in tools, +MCP/plugin tools, and tools supplied by other client connections are unaffected. +Passing an empty list removes this client's custom tools. + +Handlers switch after the runtime accepts the replacement. Running tool calls +finish on the handler they already captured, a rejected replacement leaves the +previous handlers in place, and concurrent `set_tools()` calls on the same +session are applied in order. + +```python +await session.set_tools([lookup_issue, lookup_customer]) +await session.set_tools([]) # remove this client's custom tools +``` + +See [Changing tools on a live session](../docs/features/changing-tools.md) for +the shared behavior and active-turn limitations. + #### Overriding Built-in Tools If you register a tool with the same name as a built-in CLI tool (e.g. `edit_file`, `read_file`), the SDK will throw an error unless you explicitly opt in by setting `overrides_built_in_tool=True`. This flag signals that you intend to replace the built-in tool with your custom implementation. @@ -527,6 +553,30 @@ async def edit_file(params: EditFileParams) -> str: # your logic ``` +An explicit `apply_patch` override can declare a root string schema with +`RootModel[str]`. The model sees a required `input` property, but the runtime +restores the scalar patch text before dispatch. Access it through `params.root`; +`ToolInvocation.arguments` also contains the string, not an `{"input": ...}` +object. This example returns trimmed patch text; replace the handler body with +your own patch implementation: + +```python +from pydantic import RootModel +from copilot import define_tool + + +@define_tool( + "apply_patch", + description="Apply a patch", + overrides_built_in_tool=True, +) +def apply_patch(params: RootModel[str]) -> str: + return params.root.strip() +``` + +String-schema `apply_patch` overrides cannot contain JSON Schema references; +use an object schema if references are needed. + #### Skipping Permission Prompts Set `skip_permission=True` on a tool definition to allow it to execute without triggering a permission prompt: @@ -886,6 +936,8 @@ async with await client.create_session( Available section IDs: `"preamble"`, `"identity"`, `"tone"`, `"tool_efficiency"`, `"environment_context"`, `"code_change_rules"`, `"guidelines"`, `"safety"`, `"tool_instructions"`, `"custom_instructions"`, `"runtime_instructions"`, `"last_instructions"`. `"identity"` and `"tool_instructions"` are section groups that target a collection of related sub-sections as a unit; use `"preamble"` to target just the identity preamble. +`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. `runtime_instructions` is a separate section: removing it does not remove `last_instructions`. + Each section override supports five string 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 from `"replace"`/`"append"`/`"prepend"` overrides is appended to additional instructions, and `"remove"` overrides are silently ignored. You can also pass a transform callback as the `action` instead of a string. The callback receives the current section content and returns the new content (sync or async): @@ -1051,6 +1103,16 @@ session = await client.resume_session( ) ``` +`allow_transcript_recovery` controls repair of a damaged transcript on resume. +It defaults to `True` in all modes; set it to `False` to reject recovery. If repaired, +`session.transcript_recovery` reports `planned_backup_path`, +`invalid_line_numbers` (including discarded torn-tail lines), and +`session_start_moved`; otherwise it is `None`. If repair is rejected, the +existing `JsonRpcError` exposes the server's `code` and `data` (with +`invalidLineNumbers` and `sessionStartMoved`) alongside its message. +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 `skip_permission=True` on the tool definition. See [Skipping Permission Prompts](#skipping-permission-prompts) under Tools. @@ -1089,6 +1151,14 @@ async with await client.create_session( Hook into session lifecycle events by providing handlers in the `hooks` configuration: ```python +from copilot import ( + SubagentStartHookInput, + SubagentStartHookOutput, + SubagentStopHookInput, + SubagentStopHookOutput, +) + + async def on_pre_tool_use(input, invocation): print(f"About to run tool: {input['toolName']}") # Return permission decision and optionally modify args @@ -1134,6 +1204,22 @@ async def on_session_end(input, invocation): print(f"Session ended: {input['reason']}") +async def on_subagent_start( + input: SubagentStartHookInput, invocation: dict[str, str] +) -> SubagentStartHookOutput: + print(f"Starting {input['agentName']}") + return {"additionalContext": "Report the complete result of your work."} + + +async def on_subagent_stop( + input: SubagentStopHookInput, invocation: dict[str, str] +) -> SubagentStopHookOutput: + print(f"{input['agentName']} finished: {input['stopReason']}") + # To ask for another subagent turn instead, return + # {"decision": "block", "reason": "Finish the remaining work"}. + return {"modifiedResponse": f"Verified result: {input['response']}"} + + async def on_error_occurred(input, invocation): print(f"Error in {input['errorContext']}: {input['error']}") return { @@ -1151,6 +1237,8 @@ async with await client.create_session( "on_user_prompt_submitted": on_user_prompt_submitted, "on_session_start": on_session_start, "on_session_end": on_session_end, + "on_subagent_start": on_subagent_start, + "on_subagent_stop": on_subagent_stop, "on_error_occurred": on_error_occurred, }, ) as session: @@ -1165,8 +1253,21 @@ async with await client.create_session( - `on_user_prompt_submitted` - Intercept user prompts. Can modify the prompt before processing. - `on_session_start` - Run logic when a session starts or resumes. - `on_session_end` - Cleanup or logging when session ends. +- `on_subagent_start` - Inspect a subagent launch and optionally prepend `additionalContext` to its prompt. +- `on_subagent_stop` - Inspect a completed subagent turn and optionally replace its `response` with `modifiedResponse` (with or without `"decision": "allow"`), or return `{"decision": "block", "reason": "..."}` to ask it to continue. - `on_error_occurred` - Handle errors with retry/skip/abort strategies. +Subagent hook inputs carry the **parent** `sessionId`, a timezone-aware `datetime` +`timestamp`, `workingDirectory` (normalized from the runtime's `cwd`), +`transcriptPath`, and `agentName`. The start hook may include +`agentDisplayName` and `agentDescription`. The stop hook also carries +`agentType`, `stopReason` (`"end_turn"`), the subagent's final `response`, and +optional `agentId`, `agentDisplayName`, and `agentDescription`. Handlers may +be synchronous or asynchronous and may return `None`. For typed handlers, +import `SubagentStartHookInput`, `SubagentStartHookOutput`, +`SubagentStartHandler`, `SubagentStopHookInput`, `SubagentStopHookOutput`, +and `SubagentStopHandler` from `copilot`. + ## Commands Register slash commands that users can invoke from the CLI TUI. When the user types `/commandName`, the SDK dispatches the event to your handler. diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index a2881f37ea..bb26e4b9c5 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -191,6 +191,7 @@ PreToolUseHookInput, PreToolUseHookOutput, ProviderConfig, + ProviderConfigModelProvider, ProviderModelConfig, ProviderTokenArgs, ReasoningSummary, @@ -208,8 +209,15 @@ SessionStartHookOutput, SessionUiApi, SessionUiCapabilities, + SubagentStartHandler, + SubagentStartHookInput, + SubagentStartHookOutput, + SubagentStopHandler, + SubagentStopHookInput, + SubagentStopHookOutput, SystemMessageConfig, ToolSearchConfig, + TranscriptRecoveryReport, UserInputHandler, UserInputRequest, UserInputResponse, @@ -222,11 +230,13 @@ create_attributed_permission_result, ) from .session_fs_provider import ( + SessionFsBinaryProvider, SessionFsFileInfo, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult, SessionFsSqliteTransactionFailure, + SessionFsWriteFailure, create_session_fs_adapter, ) from .tools import ( @@ -288,6 +298,7 @@ "CopilotClientMode", "CopilotExpAssignmentResponse", "CopilotSession", + "TranscriptRecoveryReport", "CopilotRequestContext", "CopilotRequestHandler", "CopilotWebSocketCloseStatus", @@ -388,6 +399,7 @@ "PreToolUseHookInput", "PreToolUseHookOutput", "ProviderConfig", + "ProviderConfigModelProvider", "ProviderModelConfig", "ProviderTokenArgs", "ReasoningSummary", @@ -411,9 +423,11 @@ "SessionFsConfig", "SessionFsFileInfo", "SessionFsProvider", + "SessionFsBinaryProvider", "SessionFsSqliteProvider", "SessionFsSqliteQueryResult", "SessionFsSqliteTransactionFailure", + "SessionFsWriteFailure", "SessionHooks", "SessionLimitsConfig", "SessionLifecycleEvent", @@ -431,6 +445,12 @@ "SessionUpdatedEvent", "StdioRuntimeConnection", "StopError", + "SubagentStartHandler", + "SubagentStartHookInput", + "SubagentStartHookOutput", + "SubagentStopHandler", + "SubagentStopHookInput", + "SubagentStopHookOutput", "SystemMessageConfig", "TcpRuntimeConnection", "TelemetryConfig", diff --git a/python/copilot/_cli_download.py b/python/copilot/_cli_download.py index 72424cb650..d36b6ff5ec 100644 --- a/python/copilot/_cli_download.py +++ b/python/copilot/_cli_download.py @@ -252,6 +252,7 @@ def _fetch_url_bytes(url: str, *, timeout: int) -> bytes: "app.js", "assets", "changelog.json", + "cli-main.js", "copilot", "copilot.exe", "copilot-sdk", diff --git a/python/copilot/_cli_version.py b/python/copilot/_cli_version.py index 4fd0921c2d..867d206254 100644 --- a/python/copilot/_cli_version.py +++ b/python/copilot/_cli_version.py @@ -9,6 +9,7 @@ from __future__ import annotations import platform +import re import sys # Sentinel: None means "no pinned version" (dev/editable install). @@ -16,7 +17,10 @@ # DO NOT reformat this line — the inject script matches it exactly. CLI_VERSION: str | None = None -_DOWNLOAD_BASE_URL = "https://github.com/github/copilot-cli/releases/download" +_UNSTABLE_VERSION = re.compile( + r"^(?:0|[1-9][0-9]*)(?:\.(?:0|[1-9][0-9]*)){2}" + r"(?:-unstable|-(?:0|[1-9][0-9]*)\.unstable)\.r[1-9][0-9]*\.g[0-9a-f]{7}$" +) # Maps (sys.platform, platform.machine()) to the platform segment used by release assets. RUNTIME_PLATFORMS: dict[tuple[str, str], str] = { @@ -61,16 +65,19 @@ def get_download_url(version: str, archive_name: str) -> str: """Return the download URL for a given version and archive.""" import os - base = os.environ.get("COPILOT_CLI_DOWNLOAD_BASE_URL", _DOWNLOAD_BASE_URL).rstrip("/") - return f"{base}/v{version}/{archive_name}" + unstable = _UNSTABLE_VERSION.fullmatch(version) is not None + repository = "copilot-sdk" if unstable else "copilot-cli" + tag = f"runtime-{version}" if unstable else f"v{version}" + base = os.environ.get( + "COPILOT_CLI_DOWNLOAD_BASE_URL", + f"https://github.com/github/{repository}/releases/download", + ).rstrip("/") + return f"{base}/{tag}/{archive_name}" def get_checksums_url(version: str) -> str: """Return the URL for the SHA256SUMS.txt file.""" - import os - - base = os.environ.get("COPILOT_CLI_DOWNLOAD_BASE_URL", _DOWNLOAD_BASE_URL).rstrip("/") - return f"{base}/v{version}/SHA256SUMS.txt" + return get_download_url(version, "SHA256SUMS.txt") def get_runtime_platform() -> str: diff --git a/python/copilot/_ffi_runtime_host.py b/python/copilot/_ffi_runtime_host.py index 8674ed6ebc..a9c77224f8 100644 --- a/python/copilot/_ffi_runtime_host.py +++ b/python/copilot/_ffi_runtime_host.py @@ -493,7 +493,8 @@ def dispose(self) -> None: """Close the FFI connection, shut down the native host, release resources. Idempotent. Callback state remains rooted until connection_close reports - that native callbacks are quiescent. + that native callbacks are quiescent. Native shutdown may block; async + callers must run this method off the event loop. """ with self._dispose_lock: if self._disposed: diff --git a/python/copilot/canvas.py b/python/copilot/canvas.py index 9b8dec5258..9beab562d7 100644 --- a/python/copilot/canvas.py +++ b/python/copilot/canvas.py @@ -188,5 +188,12 @@ async def on_close(self, ctx: CanvasProviderCloseRequest) -> None: """Canvas was closed by the user or agent. Default: no-op.""" async def on_action(self, ctx: CanvasProviderInvokeActionRequest) -> Any: - """Handle a non-lifecycle action declared by the canvas.""" + """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, return a + tool-result dict in wire form (``textResultForLlm``, ``resultType`` and + ``binaryResultsForLlm``); any other value is rendered to the model as + JSON text. + """ raise CanvasError.no_handler() diff --git a/python/copilot/client.py b/python/copilot/client.py index 0e18cb0a93..5f6f69cb4f 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -131,9 +131,11 @@ SessionLimitsConfig, SystemMessageConfig, ToolSearchConfig, + TranscriptRecoveryReport, UserInputHandler, _capabilities_to_dict, _PermissionHandlerFn, + _tools_to_wire_definitions, ) from .session_fs_provider import SessionFsProvider, create_session_fs_adapter from .tools import Tool @@ -2183,7 +2185,7 @@ async def stop(self) -> None: # Dispose the in-process FFI host and release the loaded native library. if self._ffi_host is not None: try: - self._ffi_host.dispose() + await asyncio.to_thread(self._ffi_host.dispose) except Exception: logger.debug("Error while disposing in-process FFI host", exc_info=True) self._ffi_host = None @@ -2268,7 +2270,8 @@ async def force_stop(self) -> None: # Close the transport first to signal the server immediately. # For external servers (TCP), this closes the socket. # For spawned processes (stdio), this kills the process. - if self._process is not None or self._cli_process is not None: + # The FFI adapter is disposed off-thread below. + if self._ffi_host is None and (self._process is not None or self._cli_process is not None): try: if self._is_external_server: if self._process is not None: @@ -2288,7 +2291,7 @@ async def force_stop(self) -> None: # Force-dispose the in-process FFI host before tearing down JSON-RPC. if self._ffi_host is not None: try: - self._ffi_host.dispose() + await asyncio.to_thread(self._ffi_host.dispose) except Exception: logger.debug("Error while force-disposing in-process FFI host", exc_info=True) self._ffi_host = None @@ -2616,26 +2619,7 @@ async def create_session( if not self._client: await self.start() - tool_defs = [] - if tools: - for tool in tools: - definition: dict[str, Any] = { - "name": tool.name, - "description": tool.description, - } - if tool.parameters: - definition["parameters"] = tool.parameters - if tool.overrides_built_in_tool: - definition["overridesBuiltInTool"] = True - if tool.skip_permission: - definition["skipPermission"] = True - if tool.defer is not None: - definition["defer"] = tool.defer - if tool.metadata is not None: - definition["metadata"] = tool.metadata - if tool.is_terminal: - definition["isTerminal"] = True - tool_defs.append(definition) + tool_defs = [definition.to_dict() for definition in _tools_to_wire_definitions(tools)] # Empty-mode validation and normalization mode = self._options.mode @@ -2974,6 +2958,14 @@ def _initialize_session(sid: str) -> CopilotSession: "SessionFs capabilities declare SQLite support but the provider " "does not implement SessionFsSqliteProvider" ) + if caps and caps.get("binary"): + from .session_fs_provider import SessionFsBinaryProvider + + if not isinstance(fs_provider, SessionFsBinaryProvider): + raise ValueError( + "SessionFs capabilities declare binary support but the provider " + "does not implement SessionFsBinaryProvider" + ) s._client_session_apis.session_fs = create_session_fs_adapter(fs_provider) s._register_tools(tools) s._register_commands(commands) @@ -3012,6 +3004,20 @@ def _initialize_session(sid: str) -> CopilotSession: session: CopilotSession | None = None registered_session_id: str | None = None + server_assigned_session_id: str | None = None + inline_state_lock = threading.Lock() + request_failed = False + event_loop = asyncio.get_running_loop() + + async def _delete_uninitialized_session(sid: str) -> None: + try: + await asyncio.wait_for(self.delete_session(sid), timeout=10) + except Exception: + logger.warning( + "Failed to delete cloud session %s after creation failed", + sid, + exc_info=True, + ) # Pre-register non-cloud sessions BEFORE issuing the RPC so any # session-scoped requests the CLI emits during session.create @@ -3036,15 +3042,40 @@ def _initialize_session(sid: str) -> CopilotSession: # would silently drop because the session id isn't yet # registered. Non-cloud sessions are already registered above. def _register_inline(raw_response: Any) -> None: - nonlocal session, registered_session_id + nonlocal session, registered_session_id, server_assigned_session_id if session is not None: return if not isinstance(raw_response, dict): return sid = raw_response.get("sessionId") if isinstance(sid, str) and sid: - session = _initialize_session(sid) - registered_session_id = sid + try: + initialized = _initialize_session(sid) + except Exception: + with inline_state_lock: + late_failure = request_failed + if not late_failure: + server_assigned_session_id = sid + if late_failure: + cleanup = _delete_uninitialized_session(sid) + try: + asyncio.run_coroutine_threadsafe(cleanup, event_loop) + except RuntimeError: + cleanup.close() + logger.warning( + "Failed to schedule cleanup of cloud session %s", + sid, + exc_info=True, + ) + raise + with inline_state_lock: + late_success = request_failed + if not late_success: + session = initialized + registered_session_id = sid + if late_success: + with self._sessions_lock: + self._sessions.pop(sid, None) response = await self._client.request( "session.create", payload, on_response_inline=_register_inline @@ -3075,9 +3106,15 @@ def _register_inline(raw_response: Any) -> None: capabilities = response.get("capabilities") session._set_capabilities(capabilities) except BaseException as exc: - if registered_session_id is not None: + with inline_state_lock: + request_failed = True + registered_id = registered_session_id + orphaned_id = server_assigned_session_id + if registered_id is not None: with self._sessions_lock: - self._sessions.pop(registered_session_id, None) + self._sessions.pop(registered_id, None) + if orphaned_id is not None: + await _delete_uninitialized_session(orphaned_id) self._unregister_github_token_provider(github_token_provider_registration_id) if not isinstance(exc, asyncio.CancelledError): log_timing( @@ -3189,6 +3226,7 @@ async def resume_session( remote_session: RemoteSessionMode | None = None, continue_pending_work: bool | None = None, suppress_resume_event: bool | None = None, + allow_transcript_recovery: bool | None = None, canvases: list[CanvasDeclaration] | None = None, request_canvas_renderer: bool | None = None, request_extensions: bool | None = None, @@ -3348,9 +3386,14 @@ async def resume_session( continue_pending_work: When True, instructs the runtime to continue any tool calls or permission prompts that were still pending when the session was last suspended. When False (the default), the runtime - treats pending work as interrupted on resume. + treats pending work as interrupted on resume. Completed tool results + already durably recorded by the runtime are preserved. suppress_resume_event: When True, skips emitting the session.resume event when attaching to an existing session. Defaults to False. + allow_transcript_recovery: 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 + ``session.transcript_recovery`` for the reported affected lines. feature_flags: Feature-flag values resolved by the host to apply on resume. Sent on the wire as ``featureFlags``. exp_assignments: ExP assignment ("flight") data injected by a @@ -3410,26 +3453,7 @@ async def resume_session( if not self._client: await self.start() - tool_defs = [] - if tools: - for tool in tools: - definition: dict[str, Any] = { - "name": tool.name, - "description": tool.description, - } - if tool.parameters: - definition["parameters"] = tool.parameters - if tool.overrides_built_in_tool: - definition["overridesBuiltInTool"] = True - if tool.skip_permission: - definition["skipPermission"] = True - if tool.defer is not None: - definition["defer"] = tool.defer - if tool.metadata is not None: - definition["metadata"] = tool.metadata - if tool.is_terminal: - definition["isTerminal"] = True - tool_defs.append(definition) + tool_defs = [definition.to_dict() for definition in _tools_to_wire_definitions(tools)] # Empty-mode validation and normalization mode = self._options.mode @@ -3591,6 +3615,8 @@ async def resume_session( if continue_pending_work is not None: payload["continuePendingWork"] = continue_pending_work + if allow_transcript_recovery is not None: + payload["allowTranscriptRecovery"] = allow_transcript_recovery if suppress_resume_event is not None: payload["disableResume"] = suppress_resume_event @@ -3703,6 +3729,14 @@ async def resume_session( "SessionFs capabilities declare SQLite support but the provider " "does not implement SessionFsSqliteProvider" ) + if caps and caps.get("binary"): + from .session_fs_provider import SessionFsBinaryProvider + + if not isinstance(fs_provider, SessionFsBinaryProvider): + raise ValueError( + "SessionFs capabilities declare binary support but the provider " + "does not implement SessionFsBinaryProvider" + ) session._client_session_apis.session_fs = create_session_fs_adapter(fs_provider) session._register_tools(tools) session._register_commands(commands) @@ -3761,6 +3795,13 @@ async def resume_session( session_id=session_id, ) session._workspace_path = response.get("workspacePath") + recovery = response.get("transcriptRecovery") + if recovery is not None: + session.transcript_recovery = TranscriptRecoveryReport( + planned_backup_path=recovery["plannedBackupPath"], + invalid_line_numbers=recovery["invalidLineNumbers"], + session_start_moved=recovery["sessionStartMoved"], + ) capabilities = response.get("capabilities") session._set_capabilities(capabilities) open_canvases_raw = response.get("openCanvases") @@ -3809,7 +3850,7 @@ async def resume_session( ) return session - async def start_ahp_host(self, options: AhpHostOptions | None = None) -> AhpHost: + async def start_ahp_host(self, options: AhpHostOptions) -> AhpHost: """Start an experimental, connection-owned in-process AHP listener. Factories preserve host-selected settings while adding application @@ -3817,11 +3858,13 @@ async def start_ahp_host(self, options: AhpHostOptions | None = None) -> AhpHost If canceled during startup, cleanup continues on the owning connection and disposes the listener once startup settles. """ + if options is None or (options.local_server is None and options.github_environment is None): + raise ValueError("At least one of local_server or github_environment is required") if self._state != "connected": await self.start() if self._client is None: raise RuntimeError("Client not connected") - return await self._ahp_hosts.start(self._client, options or AhpHostOptions()) + return await self._ahp_hosts.start(self._client, options) async def ping(self, message: str | None = None) -> PingResponse: """ @@ -4300,6 +4343,8 @@ def _convert_provider_to_wire_format( wire_provider["wireApi"] = provider["wire_api"] if "transport" in provider: wire_provider["transport"] = provider["transport"] + if "model_provider" in provider: + wire_provider["modelProvider"] = provider["model_provider"] if "bearer_token" in provider: wire_provider["bearerToken"] = provider["bearer_token"] if provider.get("bearer_token_provider") is not None: @@ -4334,6 +4379,8 @@ def _convert_named_provider_to_wire_format( wire["type"] = provider["type"] if "wire_api" in provider: wire["wireApi"] = provider["wire_api"] + if "model_provider" in provider: + wire["modelProvider"] = provider["model_provider"] if "base_url" in provider: wire["baseUrl"] = provider["base_url"] if "api_key" in provider: diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index a4e4f13b8b..499f4baf00 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -301,8 +301,11 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. class AuthInfoType(Enum): - """Authentication type""" + """Authentication type + Authentication type. + """ + ACCOUNT = "account" API_KEY = "api-key" COPILOT_API_TOKEN = "copilot-api-token" ENV = "env" @@ -345,6 +348,9 @@ def to_dict(self) -> dict: result["token"] = from_union([from_str, from_none], self.token) return result +class AccountAuthInfoType(Enum): + ACCOUNT = "account" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AccountGetCurrentAuthResult: @@ -460,6 +466,8 @@ class AccountKind(Enum): The provider kind of this account. + Provider kind that owns this account choice. + The neutral provider kind. A provider a consumer may interactively sign in with. @@ -841,12 +849,13 @@ class AuthLoginStepKind(Enum): # Experimental: this type is part of an experimental API and may change or be removed. class AuthLoginResultStatus(Enum): - """Terminal disposition of the login. + """Current disposition of the login, including pending user decisions. - Terminal disposition of a login persistence attempt. + Disposition of a login attempt, including pending user decisions. """ COMPLETED = "completed" DECLINED = "declined" + NEEDS_ACCOUNT_SELECTION = "needs-account-selection" NEEDS_PLAINTEXT_CONSENT = "needs-plaintext-consent" # Experimental: this type is part of an experimental API and may change or be removed. @@ -2487,17 +2496,22 @@ class ConnectorReconcileRequest: """When true, refresh the catalog before reconciling. A disabled Connector API performs no service request. """ + force_connector_name: str | None = field(default=None, kw_only=True) + """Optional Connector name to reinitialize. Requires the targetedReconcile capability.""" @staticmethod def from_dict(obj: Any) -> 'ConnectorReconcileRequest': assert isinstance(obj, dict) account_id = from_str(obj.get("accountId")) + force_connector_name = from_union([from_str, from_none], obj.get("forceConnectorName")) refresh_catalog = from_union([from_bool, from_none], obj.get("refreshCatalog")) - return ConnectorReconcileRequest(account_id, refresh_catalog) + return ConnectorReconcileRequest(account_id, refresh_catalog, force_connector_name=force_connector_name) def to_dict(self) -> dict: result: dict = {} result["accountId"] = from_str(self.account_id) + if self.force_connector_name is not None: + result["forceConnectorName"] = from_union([from_str, from_none], self.force_connector_name) if self.refresh_catalog is not None: result["refreshCatalog"] = from_union([from_bool, from_none], self.refresh_catalog) return result @@ -2896,6 +2910,11 @@ class CurrentModel: """Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. """ + plan_base_model_id: str | None = None + """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. + """ reasoning_effort: str | None = None """Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the @@ -2910,8 +2929,9 @@ def from_dict(obj: Any) -> 'CurrentModel': context_tier = from_union([ContextTier, from_none], obj.get("contextTier")) model_id = from_union([from_str, from_none], obj.get("modelId")) pending_auto_tier = from_union([AutoTier, from_none], obj.get("pendingAutoTier")) + plan_base_model_id = from_union([from_str, from_none], obj.get("planBaseModelId")) reasoning_effort = from_union([from_str, from_none], obj.get("reasoningEffort")) - return CurrentModel(activating_auto_tier, auto_tier, context_tier, model_id, pending_auto_tier, reasoning_effort) + return CurrentModel(activating_auto_tier, auto_tier, context_tier, model_id, pending_auto_tier, plan_base_model_id, reasoning_effort) def to_dict(self) -> dict: result: dict = {} @@ -2925,10 +2945,55 @@ def to_dict(self) -> dict: result["modelId"] = from_union([from_str, from_none], self.model_id) if self.pending_auto_tier is not None: result["pendingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.pending_auto_tier) + if self.plan_base_model_id is not None: + result["planBaseModelId"] = from_union([from_str, from_none], self.plan_base_model_id) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort) return result +# Experimental: this type is part of an experimental API and may change or be removed. +class CustomizationReloadStatus(Enum): + """Whether the component reloaded, was skipped, or failed + + Result of reloading a customization component. + """ + FAILED = "failed" + RELOADED = "reloaded" + SKIPPED = "skipped" + + @classmethod + def _missing_(cls, value: object) -> CustomizationReloadStatus | 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) + +# Experimental: this type is part of an experimental API and may change or be removed. +class CustomizationReloadSubsystem(Enum): + """Component whose reload was attempted or skipped + + Component of session customization discovery. + """ + AGENTS = "agents" + EXTENSIONS = "extensions" + HOOKS = "hooks" + INSTRUCTIONS = "instructions" + MCP = "mcp" + PLUGINS = "plugins" + REPOSITORY_CONTEXT = "repositoryContext" + SKILLS = "skills" + + @classmethod + def _missing_(cls, value: object) -> CustomizationReloadSubsystem | 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) + # Experimental: this type is part of an experimental API and may change or be removed. class DebugCollectLogsSource(Enum): """Source category for this entry. @@ -3254,6 +3319,142 @@ class DiscoveredMCPServerType(Enum): SSE = "sse" STDIO = "stdio" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelCapabilitiesLimitsVision: + """Vision-specific limits""" + + max_prompt_image_size: int + """Maximum image size in bytes""" + + max_prompt_images: int + """Maximum number of images per prompt""" + + supported_media_types: list[str] + """MIME types the model accepts""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelCapabilitiesLimitsVision': + assert isinstance(obj, dict) + max_prompt_image_size = from_int(obj.get("max_prompt_image_size")) + max_prompt_images = from_int(obj.get("max_prompt_images")) + supported_media_types = from_list(from_str, obj.get("supported_media_types")) + return ModelCapabilitiesLimitsVision(max_prompt_image_size, max_prompt_images, supported_media_types) + + def to_dict(self) -> dict: + result: dict = {} + result["max_prompt_image_size"] = from_int(self.max_prompt_image_size) + result["max_prompt_images"] = from_int(self.max_prompt_images) + result["supported_media_types"] = from_list(from_str, self.supported_media_types) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelArtifactDetails: + """Provider-reported model artifact details. + + Provider-reported model artifact metadata. + """ + architecture: str | None = None + """Provider-reported model architecture.""" + + families: list[str] | None = None + """Provider-reported model families.""" + + family: str | None = None + """Primary model family.""" + + format: str | None = None + """Artifact format, such as `gguf`.""" + + parameter_size: str | None = None + """Provider-reported parameter count label.""" + + quantization: str | None = None + """Provider-reported quantization label.""" + + tokenizer: str | None = None + """Provider-reported tokenizer.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelArtifactDetails': + assert isinstance(obj, dict) + architecture = from_union([from_str, from_none], obj.get("architecture")) + families = from_union([lambda x: from_list(from_str, x), from_none], obj.get("families")) + family = from_union([from_str, from_none], obj.get("family")) + format = from_union([from_str, from_none], obj.get("format")) + parameter_size = from_union([from_str, from_none], obj.get("parameterSize")) + quantization = from_union([from_str, from_none], obj.get("quantization")) + tokenizer = from_union([from_str, from_none], obj.get("tokenizer")) + return ModelArtifactDetails(architecture, families, family, format, parameter_size, quantization, tokenizer) + + def to_dict(self) -> dict: + result: dict = {} + if self.architecture is not None: + result["architecture"] = from_union([from_str, from_none], self.architecture) + if self.families is not None: + result["families"] = from_union([lambda x: from_list(from_str, x), from_none], self.families) + if self.family is not None: + result["family"] = from_union([from_str, from_none], self.family) + if self.format is not None: + result["format"] = from_union([from_str, from_none], self.format) + if self.parameter_size is not None: + result["parameterSize"] = from_union([from_str, from_none], self.parameter_size) + if self.quantization is not None: + result["quantization"] = from_union([from_str, from_none], self.quantization) + if self.tokenizer is not None: + result["tokenizer"] = from_union([from_str, from_none], self.tokenizer) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class ModelProviderProvenanceSource(Enum): + """Kind of component that supplied the adapter. + + Kind of component that supplied a provider adapter or row. Attribution does not confer + authority. + + Kind of component that supplied the adapter. Attribution does not confer authority. + """ + BUILT_IN = "builtIn" + CONFIGURED = "configured" + CUSTOM = "custom" + EXTENSION = "extension" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderWarning: + """A non-fatal provider observation warning.""" + + code: str + """Machine-readable warning code.""" + + message: str + """Human-readable warning message.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderWarning': + assert isinstance(obj, dict) + code = from_str(obj.get("code")) + message = from_str(obj.get("message")) + return ModelProviderWarning(code, message) + + def to_dict(self) -> dict: + result: dict = {} + result["code"] = from_str(self.code) + result["message"] = from_str(self.message) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class ModelProviderOperationOutcomeCode(Enum): + """Machine-readable operation outcome. + + Typed outcome for a provider operation. + """ + ABSENT = "absent" + FAILED = "failed" + SUCCESS = "success" + UNREACHABLE = "unreachable" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class EnqueueCommandParams: @@ -3280,22 +3481,73 @@ def to_dict(self) -> dict: result["displayText"] = from_union([from_none, from_str], self.display_text) return result +class EnvAuthInfoType(Enum): + ENV = "env" + # Experimental: this type is part of an experimental API and may change or be removed. -class EntraTokenInteraction(Enum): - """Whether the broker may show interaction. +class EnvironmentKind(Enum): + """GitHub Mission Control compute kind. + + Compute kind reported by Mission Control. - How far OneAuth may go to acquire the requested token. + Restrict discovery to this compute kind. """ - FORCE_INTERACTIVE = "force-interactive" - INTERACTIVE = "interactive" - SILENT = "silent" + MANAGED_ACTIONS = "managed-actions" + MANAGED_CCA = "managed-cca" + MANAGED_SANDBOX = "managed-sandbox" + USER_CODESPACE = "user-codespace" + USER_LOCAL = "user-local" -class EntraTokenAcquireResultStatus(Enum): - INTERACTION_REQUIRED = "interaction-required" - OK = "ok" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class EnvironmentsDeleteRequest: + """Identify a user-managed Mission Control environment to delete.""" -class EnvAuthInfoType(Enum): - ENV = "env" + environment_id: str + """User-managed environment to delete. GitHub-managed environments cannot be deleted.""" + + @staticmethod + def from_dict(obj: Any) -> 'EnvironmentsDeleteRequest': + assert isinstance(obj, dict) + environment_id = from_str(obj.get("environmentId")) + return EnvironmentsDeleteRequest(environment_id) + + def to_dict(self) -> dict: + result: dict = {} + result["environmentId"] = from_str(self.environment_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class EnvironmentsDeleteResult: + """Acknowledgement that the requested environment was deleted.""" + @staticmethod + def from_dict(obj: Any) -> 'EnvironmentsDeleteResult': + assert isinstance(obj, dict) + return EnvironmentsDeleteResult() + + def to_dict(self) -> dict: + result: dict = {} + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class EnvironmentsGetRequest: + """Identify a Mission Control environment to retrieve.""" + + environment_id: str + """Identifier assigned by Mission Control.""" + + @staticmethod + def from_dict(obj: Any) -> 'EnvironmentsGetRequest': + assert isinstance(obj, dict) + environment_id = from_str(obj.get("environmentId")) + return EnvironmentsGetRequest(environment_id) + + def to_dict(self) -> dict: + result: dict = {} + result["environmentId"] = from_str(self.environment_id) + return result # Experimental: this type is part of an experimental API and may change or be removed. class EventsAgentScope(Enum): @@ -4224,9 +4476,40 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class HostConfiguration: - """Normalized listener settings delivered only to the supervised hosting participant.""" +class HostGitHubEnvironmentOptions: + """Requested GitHub Mission Control registration. + + GitHub Mission Control registration options. The compute ID is application-owned and + stable. + + Registers a GitHub Mission Control environment and enables its relay transport. + """ + compute_id: str + """Stable application installation identity, reused across host restarts.""" + name: str + """Human-readable environment display name.""" + + @staticmethod + def from_dict(obj: Any) -> 'HostGitHubEnvironmentOptions': + assert isinstance(obj, dict) + compute_id = from_str(obj.get("computeId")) + name = from_str(obj.get("name")) + return HostGitHubEnvironmentOptions(compute_id, name) + + def to_dict(self) -> dict: + result: dict = {} + result["computeId"] = from_str(self.compute_id) + result["name"] = from_str(self.name) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class HostLocalServerConfiguration: + """Normalized local listener settings, absent for relay-only hosts. + + Normalized local WebSocket listener settings. + """ hostname: str """Hostname or IP address to bind.""" @@ -4236,35 +4519,23 @@ class HostConfiguration: require_connection_token: bool """Whether the listener requires token authentication.""" - resume_factory: bool | None = None - """Whether app-owned durable sessions are resumed by the owning application.""" - - session_factory: bool | None = None - """Whether session materialization is delegated to the owning application.""" - token: str | None = None """Secret connection token, absent when authentication is disabled.""" @staticmethod - def from_dict(obj: Any) -> 'HostConfiguration': + def from_dict(obj: Any) -> 'HostLocalServerConfiguration': assert isinstance(obj, dict) hostname = from_str(obj.get("hostname")) port = from_int(obj.get("port")) require_connection_token = from_bool(obj.get("requireConnectionToken")) - resume_factory = from_union([from_bool, from_none], obj.get("resumeFactory")) - session_factory = from_union([from_bool, from_none], obj.get("sessionFactory")) token = from_union([from_str, from_none], obj.get("token")) - return HostConfiguration(hostname, port, require_connection_token, resume_factory, session_factory, token) + return HostLocalServerConfiguration(hostname, port, require_connection_token, token) def to_dict(self) -> dict: result: dict = {} result["hostname"] = from_str(self.hostname) result["port"] = from_int(self.port) result["requireConnectionToken"] = from_bool(self.require_connection_token) - if self.resume_factory is not None: - result["resumeFactory"] = from_union([from_bool, from_none], self.resume_factory) - if self.session_factory is not None: - result["sessionFactory"] = from_union([from_bool, from_none], self.session_factory) if self.token is not None: result["token"] = from_union([from_str, from_none], self.token) return result @@ -4301,6 +4572,40 @@ def to_dict(self) -> dict: result: dict = {} return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class HostEnvironmentCredentials: + """Private credentials delivered only to a runtime-owned Mission Control hosting participant.""" + + github_api_url: str + """GitHub API base URL for the authenticated service.""" + + github_host: str + """Hostname of the authenticated GitHub service.""" + + mission_control_url: str + """Mission Control API origin for environment registration and management.""" + + token: str + """Current bearer token for the authenticated GitHub identity.""" + + @staticmethod + def from_dict(obj: Any) -> 'HostEnvironmentCredentials': + assert isinstance(obj, dict) + github_api_url = from_str(obj.get("githubApiUrl")) + github_host = from_str(obj.get("githubHost")) + mission_control_url = from_str(obj.get("missionControlUrl")) + token = from_str(obj.get("token")) + return HostEnvironmentCredentials(github_api_url, github_host, mission_control_url, token) + + def to_dict(self) -> dict: + result: dict = {} + result["githubApiUrl"] = from_str(self.github_api_url) + result["githubHost"] = from_str(self.github_host) + result["missionControlUrl"] = from_str(self.mission_control_url) + result["token"] = from_str(self.token) + return result + class HostExitReason(Enum): """Cause of termination.""" @@ -4309,6 +4614,46 @@ class HostExitReason(Enum): OWNER_DISCONNECTED = "ownerDisconnected" RUNTIME_SHUTDOWN = "runtimeShutdown" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class HostLocalServerOptions: + """Local WebSocket transport options. + + Enables a local WebSocket listener. + """ + hostname: str | None = None + """Listener hostname. Defaults to 127.0.0.1; explicit non-loopback binds are allowed.""" + + port: int | None = None + """Listener port. Omitted or zero requests an OS-allocated port.""" + + require_connection_token: bool | None = None + """Require token authentication (default true). Cannot be false with a token.""" + + token: str | None = None + """Nonempty connection token. Generated randomly when required and omitted.""" + + @staticmethod + def from_dict(obj: Any) -> 'HostLocalServerOptions': + assert isinstance(obj, dict) + hostname = from_union([from_str, from_none], obj.get("hostname")) + port = from_union([from_int, from_none], obj.get("port")) + require_connection_token = from_union([from_bool, from_none], obj.get("requireConnectionToken")) + token = from_union([from_str, from_none], obj.get("token")) + return HostLocalServerOptions(hostname, port, require_connection_token, token) + + def to_dict(self) -> dict: + result: dict = {} + if self.hostname is not None: + result["hostname"] = from_union([from_str, from_none], self.hostname) + if self.port is not None: + result["port"] = from_union([from_int, from_none], self.port) + if self.require_connection_token is not None: + result["requireConnectionToken"] = from_union([from_bool, from_none], self.require_connection_token) + if self.token is not None: + result["token"] = from_union([from_str, from_none], self.token) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class HostPublishSessionRequest: @@ -4362,22 +4707,29 @@ def to_dict(self) -> dict: class HostReadyRequest: """Readiness reported by the supervised hosting participant on its own SDK connection.""" - address: str + address: str | None = None """Actual bound WebSocket URL.""" + environment_id: str | None = None + """Registered environment ID, reported only once the relay transport is connected.""" + token: str | None = None """Configured secret token, absent when authentication is disabled.""" @staticmethod def from_dict(obj: Any) -> 'HostReadyRequest': assert isinstance(obj, dict) - address = from_str(obj.get("address")) + address = from_union([from_str, from_none], obj.get("address")) + environment_id = from_union([from_str, from_none], obj.get("environmentId")) token = from_union([from_str, from_none], obj.get("token")) - return HostReadyRequest(address, token) + return HostReadyRequest(address, environment_id, token) def to_dict(self) -> dict: result: dict = {} - result["address"] = from_str(self.address) + if self.address is not None: + result["address"] = from_union([from_str, from_none], self.address) + if self.environment_id is not None: + result["environmentId"] = from_union([from_str, from_none], self.environment_id) if self.token is not None: result["token"] = from_union([from_str, from_none], self.token) return result @@ -4490,61 +4842,6 @@ def to_dict(self) -> dict: result["hostId"] = from_str(self.host_id) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class HostStartRequest: - """Starts a supervised AHP listener in the runtime's configured working directory.""" - - host_id: str - """Caller-generated UUID identifying this connection-owned listener.""" - - hostname: str | None = None - """Listener hostname. Defaults to 127.0.0.1; explicit non-loopback binds are allowed.""" - - port: int | None = None - """Listener port. Omitted or zero requests an OS-allocated port.""" - - require_connection_token: bool | None = None - """Require token authentication (default true). Cannot be false with a token.""" - - resume_factory: bool | None = None - """Ask the owning application to resume its durable AHP sessions.""" - - session_factory: bool | None = None - """Ask the owning SDK application to materialize AHP sessions.""" - - token: str | None = None - """Nonempty connection token. Generated randomly when required and omitted.""" - - @staticmethod - def from_dict(obj: Any) -> 'HostStartRequest': - assert isinstance(obj, dict) - host_id = from_str(obj.get("hostId")) - hostname = from_union([from_str, from_none], obj.get("hostname")) - port = from_union([from_int, from_none], obj.get("port")) - require_connection_token = from_union([from_bool, from_none], obj.get("requireConnectionToken")) - resume_factory = from_union([from_bool, from_none], obj.get("resumeFactory")) - session_factory = from_union([from_bool, from_none], obj.get("sessionFactory")) - token = from_union([from_str, from_none], obj.get("token")) - return HostStartRequest(host_id, hostname, port, require_connection_token, resume_factory, session_factory, token) - - def to_dict(self) -> dict: - result: dict = {} - result["hostId"] = from_str(self.host_id) - if self.hostname is not None: - result["hostname"] = from_union([from_str, from_none], self.hostname) - if self.port is not None: - result["port"] = from_union([from_int, from_none], self.port) - if self.require_connection_token is not None: - result["requireConnectionToken"] = from_union([from_bool, from_none], self.require_connection_token) - if self.resume_factory is not None: - result["resumeFactory"] = from_union([from_bool, from_none], self.resume_factory) - if self.session_factory is not None: - result["sessionFactory"] = from_union([from_bool, from_none], self.session_factory) - if self.token is not None: - result["token"] = from_union([from_str, from_none], self.token) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class HostStartResult: @@ -4554,8 +4851,8 @@ class HostStartResult: host_id: str """Caller-generated listener UUID.""" - url: str - """Actual bound WebSocket URL, including the allocated port.""" + environment_id: str | None = None + """GitHub Mission Control environment ID, present when its relay transport is ready.""" pid: int | None = None """Separate host process ID, when provided by a legacy runtime. Absent for in-process @@ -4564,23 +4861,30 @@ class HostStartResult: token: str | None = None """Secret connection token, absent when authentication is disabled.""" + url: str | None = None + """Actual bound WebSocket URL, including the allocated port.""" + @staticmethod def from_dict(obj: Any) -> 'HostStartResult': assert isinstance(obj, dict) host_id = from_str(obj.get("hostId")) - url = from_str(obj.get("url")) + environment_id = from_union([from_str, from_none], obj.get("environmentId")) pid = from_union([from_int, from_none], obj.get("pid")) token = from_union([from_str, from_none], obj.get("token")) - return HostStartResult(host_id, url, pid, token) + url = from_union([from_str, from_none], obj.get("url")) + return HostStartResult(host_id, environment_id, pid, token, url) def to_dict(self) -> dict: result: dict = {} result["hostId"] = from_str(self.host_id) - result["url"] = from_str(self.url) + if self.environment_id is not None: + result["environmentId"] = from_union([from_str, from_none], self.environment_id) if self.pid is not None: result["pid"] = from_union([from_int, from_none], self.pid) if self.token is not None: result["token"] = from_union([from_str, from_none], self.token) + if self.url is not None: + result["url"] = from_union([from_str, from_none], self.url) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -5098,7 +5402,7 @@ class JSONSchemaResponseFormat: name: str """Name of the output schema, subject to the provider's naming restrictions.""" - schema: Any = None + schema: Any """JSON Schema passed unchanged to the inference provider. Schemas larger than 32 MiB when JSON-encoded are rejected before admission, using the runtime's existing request-size ceiling. This is not a guarantee that the entire model request fits. Supported keywords @@ -5473,45 +5777,98 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class ManagedSettingsReadResult: - """Validated device-managed settings discovered before a session exists.""" +class ManagedSettingMeta: + """Lock state and provenance of one managed setting. - error_message: str | None = None - """Discovery or validation error text when managed settings could not be read safely.""" + Lock state and provenance of `values.autoTier`. - settings_json: Any = None - """Validated, canonical managed-settings JSON. Omitted when no managed settings were - discovered or when discovered settings failed validation. + Lock state and provenance of `values.model`. + """ + overridable: bool + """Whether users and repositories may choose a different value. `false` means policy locks + the value. + """ + source: str + """Channel that supplied this scalar value, matching a `layers[].source`: `device`, + `server`, or `policyHelper`. These scalar defaults select one winning channel, not a + mixed source. Treat unknown values as additional channels; more may be added. """ @staticmethod - def from_dict(obj: Any) -> 'ManagedSettingsReadResult': + def from_dict(obj: Any) -> 'ManagedSettingMeta': assert isinstance(obj, dict) - error_message = from_union([from_str, from_none], obj.get("errorMessage")) - settings_json = obj.get("settingsJson") - return ManagedSettingsReadResult(error_message, settings_json) + overridable = from_bool(obj.get("overridable")) + source = from_str(obj.get("source")) + return ManagedSettingMeta(overridable, source) def to_dict(self) -> dict: result: dict = {} - if self.error_message is not None: - result["errorMessage"] = from_union([from_str, from_none], self.error_message) - if self.settings_json is not None: - result["settingsJson"] = self.settings_json + result["overridable"] = from_bool(self.overridable) + result["source"] = from_str(self.source) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class ManagedSettingsChannel(Enum): + """A channel accepted by managedSettings.compose. + + The channel whose candidate document is being supplied. + """ + DEVICE = "device" + POLICY_HELPER = "policyHelper" + SERVER = "server" + +# Experimental: this type is part of an experimental API and may change or be removed. +class ManagedSettingsDiagnosticSeverity(Enum): + """Whether the finding rejects the document. + + Severity of a managed-settings validation finding. + """ + ERROR = "error" + WARNING = "warning" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ManagedSettingsLayer: + """One managed-settings channel and the document it delivered.""" + + source: str + """Channel identifier: `device` (MDM, plist, registry, or managed file), `server` (account + or organization policy), or `policyHelper` (session-local helper output, supported by + compose). Treat unknown output values as additional channels; more may be added. + """ + settings: Any = None + """Validated managed-settings document this channel delivered. Absent when the channel + delivered none. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ManagedSettingsLayer': + assert isinstance(obj, dict) + source = from_str(obj.get("source")) + settings = obj.get("settings") + return ManagedSettingsLayer(source, settings) + + def to_dict(self) -> dict: + result: dict = {} + result["source"] = from_str(self.source) + if self.settings is not None: + result["settings"] = self.settings return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ManagedSettingsResolvedData: - """Enterprise managed-settings resolution: the effective managed settings the session - applied and which channels contributed, so SDK clients can show users what is - enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on - resume, and on account switch. This is an ephemeral live snapshot (delivered to - subscribers but not persisted to the session event log), because at session start it - resolves before `session.start` is emitted. Device values take precedence over server - values, then the policy helper, per ordinary key, while permissions compose restrictively - across device, server, policy-helper, and SDK-client layers. The account-scoped - `getManagedSettings()` API does not include session-local client injection. Marked - experimental while the managed-settings surface stabilizes. + """Effective managed settings, in the same shape as `session.managedSettings.get`. + + Effective enterprise managed settings and contributing channels. Session events report + applied policy; sessionless resolve reports an account/device snapshot, and compose + reports a non-applying preview of candidate documents. Device values take precedence over + server values, then the policy helper, per ordinary key, while permissions compose + restrictively. Session-local SDK-client policy is included only in session results. + Marked experimental while the managed-settings surface stabilizes. + + Effective managed settings from the device and account (server) channels, in the same + shape as `session.managedSettings.get`, excluding session-local injection. """ bypass_permissions_disabled: bool """Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. @@ -5595,6 +5952,134 @@ def to_dict(self) -> dict: result["settings"] = self.settings return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ManagedSettingsValues: + """Typed effective values, as in `managedSettings.resolve`. + + Typed effective values of managed settings. Each field mirrors the managed-settings + schema key of the same name; more keys are added as they are typed. + + Typed effective values of managed settings, keyed like the managed-settings schema and + already resolved across channels, with the `{ "overridable": ... }` wrapper removed. + Present when policy sets at least one typed key. Keys not typed here are available in + `resolved.settings`. + """ + auto_tier: AutoTier | None = None + """Managed Auto routing preference, used when the selected model is `auto`.""" + + model: str | None = None + """Managed default model identifier, as configured. New sessions start with it; it can name + a model the account cannot use, so hosts match it against the listed models. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ManagedSettingsValues': + assert isinstance(obj, dict) + auto_tier = from_union([AutoTier, from_none], obj.get("autoTier")) + model = from_union([from_str, from_none], obj.get("model")) + return ManagedSettingsValues(auto_tier, model) + + def to_dict(self) -> dict: + result: dict = {} + if self.auto_tier is not None: + result["autoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.auto_tier) + if self.model is not None: + result["model"] = from_union([from_str, from_none], self.model) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ManagedSettingsReadResult: + """Validated device-managed settings discovered before a session exists.""" + + error_message: str | None = None + """Discovery or validation error text when managed settings could not be read safely.""" + + settings_json: Any = None + """Validated, canonical managed-settings JSON. Omitted when no managed settings were + discovered or when discovered settings failed validation. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ManagedSettingsReadResult': + assert isinstance(obj, dict) + error_message = from_union([from_str, from_none], obj.get("errorMessage")) + settings_json = obj.get("settingsJson") + return ManagedSettingsReadResult(error_message, settings_json) + + def to_dict(self) -> dict: + result: dict = {} + if self.error_message is not None: + result["errorMessage"] = from_union([from_str, from_none], self.error_message) + if self.settings_json is not None: + result["settingsJson"] = self.settings_json + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ManagedSettingsResolveRequest: + client_name: str | None = None + """Embedding client identity for server policy requests, as in session creation. Omit for + the CLI identity. + """ + git_hub_token: str | None = None + """GitHub token to resolve instead of the current account. The call fails when the token + cannot be resolved. + """ + selection_id: str | None = None + """Opaque account identifier returned by `account.getAllUsers`. When omitted, the current + account is used, or device policy only when no account is signed in. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ManagedSettingsResolveRequest': + assert isinstance(obj, dict) + client_name = from_union([from_str, from_none], obj.get("clientName")) + git_hub_token = from_union([from_str, from_none], obj.get("gitHubToken")) + selection_id = from_union([from_str, from_none], obj.get("selectionId")) + return ManagedSettingsResolveRequest(client_name, git_hub_token, selection_id) + + def to_dict(self) -> dict: + result: dict = {} + if self.client_name is not None: + result["clientName"] = from_union([from_str, from_none], self.client_name) + if self.git_hub_token is not None: + result["gitHubToken"] = from_union([from_str, from_none], self.git_hub_token) + if self.selection_id is not None: + result["selectionId"] = from_union([from_str, from_none], self.selection_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ManagedSettingsValidateRequest: + """A candidate managed-settings document to validate without applying it.""" + + content: Any + """The document to validate: a JSON object, or a string containing the document's JSON text. + Preview documents are limited to 1 MiB and 64 levels of nesting, a stricter resource + limit than delivered-policy parsing; violations are returned as diagnostics. + """ + layer: str | None = None + """Channel the document is meant for (`device`, `server`, or `policyHelper`). Some keys are + only honored in some channels; for example, a `policyHelper` registration is ignored in + policy-helper output. When omitted, no channel-specific checks run. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ManagedSettingsValidateRequest': + assert isinstance(obj, dict) + content = obj.get("content") + layer = from_union([from_str, from_none], obj.get("layer")) + return ManagedSettingsValidateRequest(content, layer) + + def to_dict(self) -> dict: + result: dict = {} + result["content"] = self.content + if self.layer is not None: + result["layer"] = from_union([from_str, from_none], self.layer) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MarketplaceAddResult: @@ -6073,7 +6558,7 @@ def to_dict(self) -> dict: class MCPConfigureGitHubRequest: """Credential-free authentication identity used to configure GitHub MCP.""" - auth_info: Any = None + auth_info: Any """Opaque runtime auth info for GitHub MCP configuration. Marked internal: an in-process runtime shape (configureGitHubMcp is a no-op over the wire). """ @@ -6108,6 +6593,34 @@ def to_dict(self) -> dict: result["changed"] = from_bool(self.changed) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPConfiguredServerState: + """Observed state from an already materialized matching server. Omitted when no live graph + has this configured server; it never determines configuration enablement. + + Observational state for a matching already materialized MCP server. + """ + status: McpServerStatus + """Observed connection status. This is not a configuration or readiness guarantee.""" + + error: str | None = None + """Observed connection error, when the materialized server failed.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPConfiguredServerState': + assert isinstance(obj, dict) + status = McpServerStatus(obj.get("status")) + error = from_union([from_str, from_none], obj.get("error")) + return MCPConfiguredServerState(status, error) + + def to_dict(self) -> dict: + result: dict = {} + result["status"] = to_enum(McpServerStatus, self.status) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPDisableRequest: @@ -6922,6 +7435,113 @@ def to_dict(self) -> dict: result["operationId"] = from_str(self.operation_id) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPPromptArgument: + """An argument accepted by an MCP prompt.""" + + name: str + """Name of the argument""" + + meta: dict[str, Any] | None = None + """Argument-level metadata""" + + additional_properties: dict[str, Any] | None = None + """Server-provided non-standard argument fields""" + + description: str | None = None + """Description of the argument""" + + required: bool | None = None + """Whether the argument is required; omission is distinct from false""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPPromptArgument': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + meta = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("_meta")) + additional_properties = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("additionalProperties")) + description = from_union([from_str, from_none], obj.get("description")) + required = from_union([from_bool, from_none], obj.get("required")) + return MCPPromptArgument(name, meta, additional_properties, description, required) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + if self.meta is not None: + result["_meta"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.meta) + if self.additional_properties is not None: + result["additionalProperties"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.additional_properties) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + if self.required is not None: + result["required"] = from_union([from_bool, from_none], self.required) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class MCPPromptRole(Enum): + """The role of the message sender + + The sender role of an MCP prompt message. + """ + ASSISTANT = "assistant" + USER = "user" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPPromptsGetRequest: + """MCP server, prompt name, and optional string-valued arguments.""" + + prompt_name: str + """The programmatic name of the prompt""" + + server_name: str + """Name of the MCP server hosting the prompt""" + + arguments: dict[str, str] | None = None + """String-valued arguments to pass to the prompt""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPPromptsGetRequest': + assert isinstance(obj, dict) + prompt_name = from_str(obj.get("promptName")) + server_name = from_str(obj.get("serverName")) + arguments = from_union([lambda x: from_dict(from_str, x), from_none], obj.get("arguments")) + return MCPPromptsGetRequest(prompt_name, server_name, arguments) + + def to_dict(self) -> dict: + result: dict = {} + result["promptName"] = from_str(self.prompt_name) + result["serverName"] = from_str(self.server_name) + if self.arguments is not None: + result["arguments"] = from_union([lambda x: from_dict(from_str, x), from_none], self.arguments) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPPromptsListRequest: + """MCP server whose prompts to enumerate.""" + + server_name: str + """Name of the MCP server whose prompts to enumerate""" + + cursor: str | None = None + """Opaque MCP pagination cursor from a prior `nextCursor` value""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPPromptsListRequest': + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + cursor = from_union([from_str, from_none], obj.get("cursor")) + return MCPPromptsListRequest(server_name, cursor) + + def to_dict(self) -> dict: + result: dict = {} + result["serverName"] = from_str(self.server_name) + if self.cursor is not None: + result["cursor"] = from_union([from_str, from_none], self.cursor) + return result + class MCPServerConfigType(Enum): """Local transport type. Defaults to stdio when omitted. @@ -6943,7 +7563,7 @@ class MCPServerConfigType(Enum): class MCPReloadWithConfigRequest: """Opaque MCP reload configuration.""" - config: Any = None + config: Any """Opaque runtime MCP reload configuration. Marked internal: an in-process runtime shape (reloadMcpServers throws over the wire). """ @@ -7802,35 +8422,6 @@ def to_dict(self) -> dict: result["outputPrice"] = from_union([to_float, from_none], self.output_price) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ModelCapabilitiesLimitsVision: - """Vision-specific limits""" - - max_prompt_image_size: int - """Maximum image size in bytes""" - - max_prompt_images: int - """Maximum number of images per prompt""" - - supported_media_types: list[str] - """MIME types the model accepts""" - - @staticmethod - def from_dict(obj: Any) -> 'ModelCapabilitiesLimitsVision': - assert isinstance(obj, dict) - max_prompt_image_size = from_int(obj.get("max_prompt_image_size")) - max_prompt_images = from_int(obj.get("max_prompt_images")) - supported_media_types = from_list(from_str, obj.get("supported_media_types")) - return ModelCapabilitiesLimitsVision(max_prompt_image_size, max_prompt_images, supported_media_types) - - def to_dict(self) -> dict: - result: dict = {} - result["max_prompt_image_size"] = from_int(self.max_prompt_image_size) - result["max_prompt_images"] = from_int(self.max_prompt_images) - result["supported_media_types"] = from_list(from_str, self.supported_media_types) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ModelMessage: @@ -7934,6 +8525,277 @@ def to_dict(self) -> dict: result["supported_media_types"] = from_union([lambda x: from_list(from_str, x), from_none], self.supported_media_types) return result +# Experimental: this type is part of an experimental API and may change or be removed. +class ModelProviderAutomaticDiscoveryMode(Enum): + """Whether automatic discovery is allowed, limited to configured providers, or + explicit-only. + + When the runtime may run an adapter without an explicit user action. + """ + AUTOMATIC = "automatic" + CONFIGURED_ONLY = "configuredOnly" + EXPLICIT = "explicit" + +# Experimental: this type is part of an experimental API and may change or be removed. +class ModelProviderDiscoveryNetworkScope(Enum): + """Maximum network scope used by this adapter during discovery. + + Network reach an adapter may use during discovery. + """ + CONFIGURED_ENDPOINT_ONLY = "configuredEndpointOnly" + INTERNET = "internet" + LOCAL_NETWORK = "localNetwork" + LOOPBACK_ONLY = "loopbackOnly" + NONE = "none" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderAdapterOperationDescriptor: + """An operation supported by a model-provider adapter.""" + + name: str + """Supported operation name: `discover`, `getStatus`, or `models.list`. Unknown names and + duplicate declarations are rejected. + """ + input_schema: Any = None + """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. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderAdapterOperationDescriptor': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + input_schema = obj.get("inputSchema") + return ModelProviderAdapterOperationDescriptor(name, input_schema) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + if self.input_schema is not None: + result["inputSchema"] = self.input_schema + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class ModelProviderConfigurationDisposition(Enum): + """Whether a planned configuration entry is new or already present in the session registry. + + Whether `model` still needs to be registered. When `alreadyConfigured`, `selectionId` is + already registered and the caller can select it without adding anything. + + 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. + """ + ALREADY_CONFIGURED = "alreadyConfigured" + CREATE = "create" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SystemMessageBlock: + content: str + """Text content for this system-message block.""" + + cache_breakpoint: bool | None = None + """Whether providers with explicit prompt caching should place a cache breakpoint after this + block. + """ + is_static: bool | None = None + """Whether the block is static and may be cached independently of dynamic prompt content.""" + + @staticmethod + def from_dict(obj: Any) -> 'SystemMessageBlock': + assert isinstance(obj, dict) + content = from_str(obj.get("content")) + cache_breakpoint = from_union([from_bool, from_none], obj.get("cacheBreakpoint")) + is_static = from_union([from_bool, from_none], obj.get("isStatic")) + return SystemMessageBlock(content, cache_breakpoint, is_static) + + def to_dict(self) -> dict: + result: dict = {} + result["content"] = from_str(self.content) + if self.cache_breakpoint is not None: + result["cacheBreakpoint"] = from_union([from_bool, from_none], self.cache_breakpoint) + if self.is_static is not None: + result["isStatic"] = from_union([from_bool, from_none], self.is_static) + return result + +class ProtocolMode(Enum): + """Append-mode discriminator. Omission also selects append mode. + + Replace-mode discriminator. + + Customize-mode discriminator. + """ + APPEND = "append" + CUSTOMIZE = "customize" + REPLACE = "replace" + +class ProtocolSectionOverrideAction(Enum): + """Declarative operation applied to the section.""" + + APPEND = "append" + PREPEND = "prepend" + PRESERVE = "preserve" + REMOVE = "remove" + REPLACE = "replace" + TRANSFORM = "transform" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ProviderConfigAzure: + """Azure authentication configuration for the provider. + + Azure-specific provider options. + """ + api_version: str | None = None + """API version. When set, uses the versioned deployment route. When omitted, uses the GA + versionless v1 route. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ProviderConfigAzure': + assert isinstance(obj, dict) + api_version = from_union([from_str, from_none], obj.get("apiVersion")) + return ProviderConfigAzure(api_version) + + def to_dict(self) -> dict: + result: dict = {} + if self.api_version is not None: + result["apiVersion"] = from_union([from_str, from_none], self.api_version) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class ProviderConfigModelProvider(Enum): + """The product serving the provider's models, reported in telemetry as `model_provider`. + Only affects telemetry. + + The product serving the model, reported in telemetry as `model_provider`. + + 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. + """ + ANTHROPIC = "anthropic" + AZURE_OPENAI = "azure_openai" + FOUNDRY_LOCAL = "foundry_local" + LLAMA_CPP = "llama_cpp" + LM_STUDIO = "lm_studio" + OLLAMA = "ollama" + OPENAI = "openai" + +# Experimental: this type is part of an experimental API and may change or be removed. +class ProviderTransport(Enum): + """Transport used to communicate with the provider. + + Provider transport. Defaults to "http". + + Transport to use for inference against this instance. + + Transport to be used for provider requests. + """ + HTTP = "http" + WEBSOCKETS = "websockets" + +# Experimental: this type is part of an experimental API and may change or be removed. +class ProviderType(Enum): + """Provider protocol family. + + Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. + + Provider family to use for inference against this instance. + + Provider family. Matches the `type` field of a BYOK provider config. + """ + ANTHROPIC = "anthropic" + AZURE = "azure" + OPENAI = "openai" + +# Experimental: this type is part of an experimental API and may change or be removed. +class ProviderWireAPI(Enum): + """Wire API used to communicate with the provider. + + Wire API format (openai/azure only). Defaults to "completions". + + Wire API to use for inference against this instance, when required by the provider + family. + + Wire API to be used, when required for the provider type. + """ + COMPLETIONS = "completions" + RESPONSES = "responses" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderDiscoverRequest: + """Provider discovery parameters.""" + + adapter_id: str + """Opaque adapter identity returned by `session.providers.getCatalog`.""" + + input: Any = None + """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. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderDiscoverRequest': + assert isinstance(obj, dict) + adapter_id = from_str(obj.get("adapterId")) + input = obj.get("input") + return ModelProviderDiscoverRequest(adapter_id, input) + + def to_dict(self) -> dict: + result: dict = {} + result["adapterId"] = from_str(self.adapter_id) + if self.input is not None: + result["input"] = self.input + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderInstanceReference: + """Self-contained reference for subsequent provider operations. + + Serializable reference to a discovered provider instance. + + Provider instance reference returned by discovery. + """ + adapter_id: str + """Stable opaque identity of the adapter that owns this reference. Must be present in the + target session's effective catalog. + """ + id: str + """Stable instance identifier derived by the provider adapter, such as + `ollama:{normalizedEndpoint}`. + """ + management_endpoint: str + """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. + """ + provider_kind: str + """Descriptive provider family. Must match the selected adapter; not a routing key.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderInstanceReference': + assert isinstance(obj, dict) + adapter_id = from_str(obj.get("adapterId")) + id = from_str(obj.get("id")) + management_endpoint = from_str(obj.get("managementEndpoint")) + provider_kind = from_str(obj.get("providerKind")) + return ModelProviderInstanceReference(adapter_id, id, management_endpoint, provider_kind) + + def to_dict(self) -> dict: + result: dict = {} + result["adapterId"] = from_str(self.adapter_id) + result["id"] = from_str(self.id) + result["managementEndpoint"] = from_str(self.management_endpoint) + result["providerKind"] = from_str(self.provider_kind) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ModelSetAllowedModelsResult: @@ -8185,64 +9047,6 @@ def to_dict(self) -> dict: result["name"] = from_str(self.name) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ProviderConfigAzure: - """Azure authentication configuration for the provider. - - Azure-specific provider options. - """ - api_version: str | None = None - """API version. When set, uses the versioned deployment route. When omitted, uses the GA - versionless v1 route. - """ - - @staticmethod - def from_dict(obj: Any) -> 'ProviderConfigAzure': - assert isinstance(obj, dict) - api_version = from_union([from_str, from_none], obj.get("apiVersion")) - return ProviderConfigAzure(api_version) - - def to_dict(self) -> dict: - result: dict = {} - if self.api_version is not None: - result["apiVersion"] = from_union([from_str, from_none], self.api_version) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -class ProviderTransport(Enum): - """Transport used to communicate with the provider. - - Provider transport. Defaults to "http". - - Transport to be used for provider requests. - """ - HTTP = "http" - WEBSOCKETS = "websockets" - -# Experimental: this type is part of an experimental API and may change or be removed. -class ProviderType(Enum): - """Provider protocol family. - - Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. - - Provider family. Matches the `type` field of a BYOK provider config. - """ - ANTHROPIC = "anthropic" - AZURE = "azure" - OPENAI = "openai" - -# Experimental: this type is part of an experimental API and may change or be removed. -class ProviderWireAPI(Enum): - """Wire API used to communicate with the provider. - - Wire API format (openai/azure only). Defaults to "completions". - - Wire API to be used, when required for the provider type. - """ - COMPLETIONS = "completions" - RESPONSES = "responses" - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class OptionsUpdateAdditionalContentExclusionPolicyRuleSource: @@ -9573,16 +10377,6 @@ class ProtocolReplaceMode(Enum): REPLACE = "replace" -class ProtocolSectionOverrideAction(Enum): - """Declarative operation applied to the section.""" - - APPEND = "append" - PREPEND = "prepend" - PRESERVE = "preserve" - REMOVE = "remove" - REPLACE = "replace" - TRANSFORM = "transform" - # Experimental: this type is part of an experimental API and may change or be removed. class ProtocolStaticSectionAction(Enum): """Declarative operation applied to the section.""" @@ -9592,47 +10386,6 @@ class ProtocolStaticSectionAction(Enum): REMOVE = "remove" REPLACE = "replace" -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class SystemMessageBlock: - content: str - """Text content for this system-message block.""" - - cache_breakpoint: bool | None = None - """Whether providers with explicit prompt caching should place a cache breakpoint after this - block. - """ - is_static: bool | None = None - """Whether the block is static and may be cached independently of dynamic prompt content.""" - - @staticmethod - def from_dict(obj: Any) -> 'SystemMessageBlock': - assert isinstance(obj, dict) - content = from_str(obj.get("content")) - cache_breakpoint = from_union([from_bool, from_none], obj.get("cacheBreakpoint")) - is_static = from_union([from_bool, from_none], obj.get("isStatic")) - return SystemMessageBlock(content, cache_breakpoint, is_static) - - def to_dict(self) -> dict: - result: dict = {} - result["content"] = from_str(self.content) - if self.cache_breakpoint is not None: - result["cacheBreakpoint"] = from_union([from_bool, from_none], self.cache_breakpoint) - if self.is_static is not None: - result["isStatic"] = from_union([from_bool, from_none], self.is_static) - return result - -class ProtocolMode(Enum): - """Append-mode discriminator. Omission also selects append mode. - - Replace-mode discriminator. - - Customize-mode discriminator. - """ - APPEND = "append" - CUSTOMIZE = "customize" - REPLACE = "replace" - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ProviderAddResult: @@ -9777,6 +10530,42 @@ def to_dict(self) -> dict: result["token"] = from_str(self.token) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ProviderWithdrawResult: + """What the withdrawal actually removed from the registry.""" + + providers_removed: list[str] + """Providers removed because one of the withdrawn models was the last entry referencing + them. A provider that merely has no models is not removed. + """ + withdrawn: list[str] + """Selection ids that were registered and are now withdrawn. Excludes requested ids that + were not present. + """ + model_deselected: bool | None = None + """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. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ProviderWithdrawResult': + assert isinstance(obj, dict) + providers_removed = from_list(from_str, obj.get("providersRemoved")) + withdrawn = from_list(from_str, obj.get("withdrawn")) + model_deselected = from_union([from_bool, from_none], obj.get("modelDeselected")) + return ProviderWithdrawResult(providers_removed, withdrawn, model_deselected) + + def to_dict(self) -> dict: + result: dict = {} + result["providersRemoved"] = from_list(from_str, self.providers_removed) + result["withdrawn"] = from_list(from_str, self.withdrawn) + if self.model_deselected is not None: + result["modelDeselected"] = from_union([from_bool, from_none], self.model_deselected) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PushGitHubRepoRef: @@ -11139,7 +11928,9 @@ def to_dict(self) -> dict: @dataclass class SandboxHostCapability: """Whether this host can run one sandbox policy feature. A session whose effective policy - uses an unsupported feature fails each sandboxed command with `reason`. + uses an unsupported feature fails each sandboxed command with `reason`, except + `filesystem_enumeration`, whose absence degrades sandboxed PowerShell instead of failing + it. """ name: str """The policy feature, as an extensible string: ignore names you do not recognize. Known @@ -11148,9 +11939,11 @@ class SandboxHostCapability: `network_filtering` (host rules and the sandbox proxy; on Linux this needs the same tooling as `network`; on Windows it needs Process Security Environment 1.1 host-loopback support, and a policy that uses it must also set `network.allowLocalNetwork`), - `denied_paths` (native enforcement of `filesystem.deniedPaths`), and `shell` (shell - commands inside the sandbox; on Windows this needs Process Security Environment 1.1 - filesystem enumeration support). + `denied_paths` (native enforcement of `filesystem.deniedPaths`), `shell` (shell commands + inside the sandbox), and `filesystem_enumeration` (enumerate-only filesystem grants; on + Windows this needs Process Security Environment 1.1 filesystem enumeration support, and + without it sandboxed PowerShell still runs but cannot resolve its current location; other + platforms always report it). """ supported: bool """Whether this host can run the feature.""" @@ -11176,6 +11969,36 @@ def to_dict(self) -> dict: result["reason"] = from_union([from_str, from_none], self.reason) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SandboxProxyCACreateResult: + """Result of creating the persistent certificate authority of the sandbox credential proxy.""" + + certificate_path: str + """Absolute path of the public certificate of the certificate authority, in PEM format.""" + + @staticmethod + def from_dict(obj: Any) -> 'SandboxProxyCACreateResult': + assert isinstance(obj, dict) + certificate_path = from_str(obj.get("certificatePath")) + return SandboxProxyCACreateResult(certificate_path) + + def to_dict(self) -> dict: + result: dict = {} + result["certificatePath"] = from_str(self.certificate_path) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class SandboxProxyCAState(Enum): + """State of the persistent certificate authority of the sandbox credential proxy. + + The state of the certificate authority. + """ + ERROR = "error" + INSTALLED = "installed" + NOT_INSTALLED = "notInstalled" + UNSUPPORTED = "unsupported" + # Experimental: this type is part of an experimental API and may change or be removed. class SandboxSessionChange(Enum): """A session-scoped sandbox transition applied while handling a slash command @@ -12026,6 +12849,30 @@ def to_dict(self) -> dict: result["recursive"] = from_union([from_bool, from_none], self.recursive) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionFSReadFileBytesRequest: + """Path of the binary file to read from the client-provided session filesystem.""" + + path: str + """Path using SessionFs conventions""" + + session_id: str + """Target session identifier""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionFSReadFileBytesRequest': + assert isinstance(obj, dict) + path = from_str(obj.get("path")) + session_id = from_str(obj.get("sessionId")) + return SessionFSReadFileBytesRequest(path, session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["path"] = from_str(self.path) + result["sessionId"] = from_str(self.session_id) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionFSReadFileRequest: @@ -12174,14 +13021,21 @@ class SessionFSSetProviderCapabilities: sqlite: bool | None = None """Whether the provider supports SQLite query/exists operations""" + binary: bool | None = field(default=None, kw_only=True) + """Whether the provider supports binary reads and writes through sessionFs.readFileBytes and + sessionFs.writeFileBytes + """ @staticmethod def from_dict(obj: Any) -> 'SessionFSSetProviderCapabilities': assert isinstance(obj, dict) + binary = from_union([from_bool, from_none], obj.get("binary")) sqlite = from_union([from_bool, from_none], obj.get("sqlite")) - return SessionFSSetProviderCapabilities(sqlite) + return SessionFSSetProviderCapabilities(sqlite, binary=binary) def to_dict(self) -> dict: result: dict = {} + if self.binary is not None: + result["binary"] = from_union([from_bool, from_none], self.binary) if self.sqlite is not None: result["sqlite"] = from_union([from_bool, from_none], self.sqlite) return result @@ -12295,6 +13149,42 @@ def to_dict(self) -> dict: result["sessionId"] = from_str(self.session_id) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionFSWriteFileBytesRequest: + """File path, standard-base64-encoded bytes to write, and optional mode for the + client-provided session filesystem. + """ + content: str + """Exact file bytes encoded as standard base64""" + + path: str + """Path using SessionFs conventions""" + + session_id: str + """Target session identifier""" + + mode: int | None = None + """Optional POSIX-style mode for newly created files""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionFSWriteFileBytesRequest': + assert isinstance(obj, dict) + content = from_str(obj.get("content")) + path = from_str(obj.get("path")) + session_id = from_str(obj.get("sessionId")) + mode = from_union([from_int, from_none], obj.get("mode")) + return SessionFSWriteFileBytesRequest(content, path, session_id, mode) + + def to_dict(self) -> dict: + result: dict = {} + result["content"] = from_str(self.content) + result["path"] = from_str(self.path) + result["sessionId"] = from_str(self.session_id) + if self.mode is not None: + result["mode"] = from_union([from_int, from_none], self.mode) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionFSWriteFileRequest: @@ -12902,6 +13792,7 @@ def to_dict(self) -> dict: return result class SettableAuthInfoType(Enum): + ACCOUNT = "account" API_KEY = "api-key" COPILOT_API_TOKEN = "copilot-api-token" ENV = "env" @@ -14081,45 +14972,14 @@ def to_dict(self) -> dict: result["requestId"] = from_str(self.request_id) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ShellExecRequest: - """Shell command to run, with optional working directory and timeout in milliseconds.""" - - command: str - """Shell command to execute""" - - cwd: str | None = None - """Working directory (defaults to session working directory)""" - - timeout: int | None = None - """Timeout in milliseconds (default: 30000)""" - - @staticmethod - def from_dict(obj: Any) -> 'ShellExecRequest': - assert isinstance(obj, dict) - command = from_str(obj.get("command")) - cwd = from_union([from_str, from_none], obj.get("cwd")) - timeout = from_union([from_int, from_none], obj.get("timeout")) - return ShellExecRequest(command, cwd, timeout) - - def to_dict(self) -> dict: - result: dict = {} - result["command"] = from_str(self.command) - if self.cwd is not None: - result["cwd"] = from_union([from_str, from_none], self.cwd) - if self.timeout is not None: - result["timeout"] = from_union([from_int, from_none], self.timeout) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ShellExecResult: - """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. """ process_id: str - """Unique identifier for tracking streamed output""" + """Identifier usable with shell.kill while the process is running""" @staticmethod def from_dict(obj: Any) -> 'ShellExecResult': @@ -17081,6 +17941,35 @@ def to_dict(self) -> dict: result["operation"] = from_str(self.operation) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AuthIdentityMetadata: + """Credential-free identity metadata.""" + + host: str + """Identity host.""" + + login: str + """User login.""" + + type: AuthInfoType + """Authentication type.""" + + @staticmethod + def from_dict(obj: Any) -> 'AuthIdentityMetadata': + assert isinstance(obj, dict) + host = from_str(obj.get("host")) + login = from_str(obj.get("login")) + type = AuthInfoType(obj.get("type")) + return AuthIdentityMetadata(host, login, type) + + def to_dict(self) -> dict: + result: dict = {} + result["host"] = from_str(self.host) + result["login"] = from_str(self.login) + result["type"] = to_enum(AuthInfoType, self.type) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionAuthStatus: @@ -17394,6 +18283,10 @@ class ModelCapabilitiesSupports: reasoning_effort: bool | None = None """Whether this model supports reasoning effort configuration""" + thinking: bool | None = None + """Whether the model supports provider-native thinking. Independent of configurable + reasoning effort; omission means unknown. + """ tool_calls: bool | None = None """Whether this model supports canonical tool calling""" @@ -17405,9 +18298,10 @@ def from_dict(obj: Any) -> 'ModelCapabilitiesSupports': assert isinstance(obj, dict) adaptive_thinking = from_union([AdaptiveThinkingSupport, from_none], obj.get("adaptive_thinking")) reasoning_effort = from_union([from_bool, from_none], obj.get("reasoningEffort")) + thinking = from_union([from_bool, from_none], obj.get("thinking")) tool_calls = from_union([from_bool, from_none], obj.get("toolCalls")) vision = from_union([from_bool, from_none], obj.get("vision")) - return ModelCapabilitiesSupports(adaptive_thinking, reasoning_effort, tool_calls, vision) + return ModelCapabilitiesSupports(adaptive_thinking, reasoning_effort, thinking, tool_calls, vision) def to_dict(self) -> dict: result: dict = {} @@ -17415,6 +18309,8 @@ def to_dict(self) -> dict: result["adaptive_thinking"] = from_union([lambda x: to_enum(AdaptiveThinkingSupport, x), from_none], self.adaptive_thinking) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_bool, from_none], self.reasoning_effort) + if self.thinking is not None: + result["thinking"] = from_union([from_bool, from_none], self.thinking) if self.tool_calls is not None: result["toolCalls"] = from_union([from_bool, from_none], self.tool_calls) if self.vision is not None: @@ -19273,6 +20169,11 @@ class ConnectorCapabilities: """Whether callers select a host-owned GitHub account through an opaque selection ID rather than supplying a provider token. """ + session_account_selection: bool | None = field(default=None, kw_only=True) + """Whether getAccount is supported. Absence means false.""" + + targeted_reconcile: bool | None = field(default=None, kw_only=True) + """Whether reconcile accepts forceConnectorName. Absence means false.""" @staticmethod def from_dict(obj: Any) -> 'ConnectorCapabilities': @@ -19284,7 +20185,9 @@ def from_dict(obj: Any) -> 'ConnectorCapabilities': max_poll_attempts = from_int(obj.get("maxPollAttempts")) max_poll_interval_ms = from_int(obj.get("maxPollIntervalMs")) opaque_account_selection = from_bool(obj.get("opaqueAccountSelection")) - return ConnectorCapabilities(api_version, availability, consent_continuation, max_deadline_ms, max_poll_attempts, max_poll_interval_ms, opaque_account_selection) + session_account_selection = from_union([from_bool, from_none], obj.get("sessionAccountSelection")) + targeted_reconcile = from_union([from_bool, from_none], obj.get("targetedReconcile")) + return ConnectorCapabilities(api_version, availability, consent_continuation, max_deadline_ms, max_poll_attempts, max_poll_interval_ms, opaque_account_selection, session_account_selection=session_account_selection, targeted_reconcile=targeted_reconcile) def to_dict(self) -> dict: result: dict = {} @@ -19295,6 +20198,10 @@ def to_dict(self) -> dict: result["maxPollAttempts"] = from_int(self.max_poll_attempts) result["maxPollIntervalMs"] = from_int(self.max_poll_interval_ms) result["opaqueAccountSelection"] = from_bool(self.opaque_account_selection) + if self.session_account_selection is not None: + result["sessionAccountSelection"] = from_union([from_bool, from_none], self.session_account_selection) + if self.targeted_reconcile is not None: + result["targetedReconcile"] = from_union([from_bool, from_none], self.targeted_reconcile) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -19317,6 +20224,15 @@ class ConnectorCatalogEntry: description: str | None = None """Untrusted service description, when present.""" + logo: str | None = field(default=None, kw_only=True) + """Optional catalog logo.""" + + release_tag: str | None = field(default=None, kw_only=True) + """Optional catalog release tag.""" + + tier: str | None = field(default=None, kw_only=True) + """Optional catalog tier.""" + @staticmethod def from_dict(obj: Any) -> 'ConnectorCatalogEntry': assert isinstance(obj, dict) @@ -19325,7 +20241,10 @@ def from_dict(obj: Any) -> 'ConnectorCatalogEntry': runtime_server_ids = from_list(from_str, obj.get("runtimeServerIds")) status = ConnectorCatalogStatus(obj.get("status")) description = from_union([from_str, from_none], obj.get("description")) - return ConnectorCatalogEntry(display_name, name, runtime_server_ids, status, description) + logo = from_union([from_str, from_none], obj.get("logo")) + release_tag = from_union([from_str, from_none], obj.get("releaseTag")) + tier = from_union([from_str, from_none], obj.get("tier")) + return ConnectorCatalogEntry(display_name, name, runtime_server_ids, status, description, logo=logo, tier=tier, release_tag=release_tag) def to_dict(self) -> dict: result: dict = {} @@ -19335,6 +20254,12 @@ def to_dict(self) -> dict: result["status"] = to_enum(ConnectorCatalogStatus, self.status) if self.description is not None: result["description"] = from_union([from_str, from_none], self.description) + if self.logo is not None: + result["logo"] = from_union([from_str, from_none], self.logo) + if self.release_tag is not None: + result["releaseTag"] = from_union([from_str, from_none], self.release_tag) + if self.tier is not None: + result["tier"] = from_union([from_str, from_none], self.tier) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -19420,35 +20345,65 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class AuthLoginResultDto: - """The terminal login result. - - Terminal result of an interactive login flow. +class AccountAuthInfo: + """An interactive account whose model provider owns its credentials. It carries no GitHub + credential. """ - status: AuthLoginResultStatus - """Terminal disposition of the login.""" + host: str + """Host coordinate owned by the account's model provider.""" - host: str | None = None - """Host that was signed in, when completed.""" + login: str + """Login identifying the provider-owned account.""" - login: str | None = None - """Login that was signed in, when completed.""" + type: ClassVar[str] = "account" + """Provider-owned account authentication.""" @staticmethod - def from_dict(obj: Any) -> 'AuthLoginResultDto': + def from_dict(obj: Any) -> 'AccountAuthInfo': assert isinstance(obj, dict) - status = AuthLoginResultStatus(obj.get("status")) - host = from_union([from_str, from_none], obj.get("host")) - login = from_union([from_str, from_none], obj.get("login")) - return AuthLoginResultDto(status, host, login) + host = from_str(obj.get("host")) + login = from_str(obj.get("login")) + return AccountAuthInfo(host, login) def to_dict(self) -> dict: result: dict = {} - result["status"] = to_enum(AuthLoginResultStatus, self.status) - if self.host is not None: - result["host"] = from_union([from_str, from_none], self.host) - if self.login is not None: - result["login"] = from_union([from_str, from_none], self.login) + result["host"] = from_str(self.host) + result["login"] = from_str(self.login) + result["type"] = self.type + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AuthLoginAccount: + """A credential-free account choice after sign-in.""" + + host: str + """Host coordinate owned by the selected account's provider.""" + + kind: AccountKind + """Provider kind that owns this account choice.""" + + login: str + """Human-readable login for the account choice.""" + + selection_id: str + """Opaque identifier supplied to the next login step to select this account.""" + + @staticmethod + def from_dict(obj: Any) -> 'AuthLoginAccount': + assert isinstance(obj, dict) + host = from_str(obj.get("host")) + kind = AccountKind(obj.get("kind")) + login = from_str(obj.get("login")) + selection_id = from_str(obj.get("selectionId")) + return AuthLoginAccount(host, kind, login, selection_id) + + def to_dict(self) -> dict: + result: dict = {} + result["host"] = from_str(self.host) + result["kind"] = to_enum(AccountKind, self.kind) + result["login"] = from_str(self.login) + result["selectionId"] = from_str(self.selection_id) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -19628,6 +20583,45 @@ def to_dict(self) -> dict: result["items"] = from_list(lambda x: to_class(SessionCompletionItem, x), self.items) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class EnvironmentCapabilities: + """Hosting capabilities and session capacity advertised by an environment. + + Hosting capabilities advertised by the environment. + """ + features: list[str] + """Feature identifiers advertised by the environment.""" + + ahp_version: str | None = None + """Advertised Agent Host Protocol version.""" + + current_sessions: int | None = None + """Current session count, when advertised.""" + + max_sessions: int | None = None + """Maximum session capacity, when advertised.""" + + @staticmethod + def from_dict(obj: Any) -> 'EnvironmentCapabilities': + assert isinstance(obj, dict) + features = from_list(from_str, obj.get("features")) + ahp_version = from_union([from_str, from_none], obj.get("ahpVersion")) + current_sessions = from_union([from_int, from_none], obj.get("currentSessions")) + max_sessions = from_union([from_int, from_none], obj.get("maxSessions")) + return EnvironmentCapabilities(features, ahp_version, current_sessions, max_sessions) + + def to_dict(self) -> dict: + result: dict = {} + result["features"] = from_list(from_str, self.features) + if self.ahp_version is not None: + result["ahpVersion"] = from_union([from_str, from_none], self.ahp_version) + if self.current_sessions is not None: + result["currentSessions"] = from_union([from_int, from_none], self.current_sessions) + if self.max_sessions is not None: + result["maxSessions"] = from_union([from_int, from_none], self.max_sessions) + return result + @dataclass class HostSessionCreateCallback: """Application callback routed over its existing SDK connection.""" @@ -19692,6 +20686,33 @@ def to_dict(self) -> dict: result["resume"] = from_union([from_bool, from_none], self.resume) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPOauthCompleteRequest: + """Host-delivered callback for a runtime-managed MCP OAuth login.""" + + authorization_id: str + """Opaque identifier returned by session.mcp.oauth.login for the pending external callback.""" + + callback_url: str + """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. + """ + + @staticmethod + def from_dict(obj: Any) -> 'MCPOauthCompleteRequest': + assert isinstance(obj, dict) + authorization_id = from_str(obj.get("authorizationId")) + callback_url = from_str(obj.get("callbackUrl")) + return MCPOauthCompleteRequest(authorization_id, callback_url) + + def to_dict(self) -> dict: + result: dict = {} + result["authorizationId"] = from_str(self.authorization_id) + result["callbackUrl"] = from_str(self.callback_url) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ModelSetAllowedModelsRequest: @@ -19718,6 +20739,57 @@ def to_dict(self) -> dict: result["allowedModels"] = from_union([lambda x: from_list(from_str, x), from_none], self.allowed_models) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ProviderWithdrawRequest: + """Host-managed model selection ids to withdraw from the session's BYOK registry.""" + + models: list[str] + """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. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ProviderWithdrawRequest': + assert isinstance(obj, dict) + models = from_list(from_str, obj.get("models")) + return ProviderWithdrawRequest(models) + + def to_dict(self) -> dict: + result: dict = {} + result["models"] = from_list(from_str, self.models) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CustomizationReloadOutcome: + """Result of one customization reload component.""" + + status: CustomizationReloadStatus + """Whether the component reloaded, was skipped, or failed""" + + subsystem: CustomizationReloadSubsystem + """Component whose reload was attempted or skipped""" + + detail: str | None = None + """Reason for a skipped component or description of a failure, when available""" + + @staticmethod + def from_dict(obj: Any) -> 'CustomizationReloadOutcome': + assert isinstance(obj, dict) + status = CustomizationReloadStatus(obj.get("status")) + subsystem = CustomizationReloadSubsystem(obj.get("subsystem")) + detail = from_union([from_none, from_str], obj.get("detail")) + return CustomizationReloadOutcome(status, subsystem, detail) + + def to_dict(self) -> dict: + result: dict = {} + result["status"] = to_enum(CustomizationReloadStatus, self.status) + result["subsystem"] = to_enum(CustomizationReloadSubsystem, self.subsystem) + if self.detail is not None: + result["detail"] = from_union([from_none, from_str], self.detail) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class DebugCollectLogsCollectedEntry: @@ -20111,86 +21183,180 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class EntraTokenAcquireRequest: - """OneAuth token request supplied by a trusted host application.""" +class ModelCapabilitiesLimits: + """Token limits for prompts, outputs, and context window""" - client_id: str - """Public client application id.""" + max_context_window_tokens: int | None = None + """Maximum total context window size in tokens""" + + max_output_tokens: int | None = None + """Maximum number of output/completion tokens""" + + max_prompt_tokens: int | None = None + """Maximum number of prompt/input tokens""" + + vision: ModelCapabilitiesLimitsVision | None = None + """Vision-specific limits""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelCapabilitiesLimits': + assert isinstance(obj, dict) + max_context_window_tokens = from_union([from_int, from_none], obj.get("max_context_window_tokens")) + max_output_tokens = from_union([from_int, from_none], obj.get("max_output_tokens")) + max_prompt_tokens = from_union([from_int, from_none], obj.get("max_prompt_tokens")) + vision = from_union([ModelCapabilitiesLimitsVision.from_dict, from_none], obj.get("vision")) + return ModelCapabilitiesLimits(max_context_window_tokens, max_output_tokens, max_prompt_tokens, vision) + + def to_dict(self) -> dict: + result: dict = {} + if self.max_context_window_tokens is not None: + result["max_context_window_tokens"] = from_union([from_int, from_none], self.max_context_window_tokens) + if self.max_output_tokens is not None: + result["max_output_tokens"] = from_union([from_int, from_none], self.max_output_tokens) + if self.max_prompt_tokens is not None: + result["max_prompt_tokens"] = from_union([from_int, from_none], self.max_prompt_tokens) + if self.vision is not None: + result["vision"] = from_union([lambda x: to_class(ModelCapabilitiesLimitsVision, x), from_none], self.vision) + return result - interaction: EntraTokenInteraction - """Whether the broker may show interaction.""" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderProvenance: + """Attribution for the adapter that produced this model row. + + Attribution for the adapter that produced a provider row. - redirect_uri: str - """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. + Attribution for the adapter that produced this instance. """ - scopes: list[str] - """Exact delegated scopes to request.""" + adapter_id: str + """Stable opaque adapter identity from the effective catalog. Treat this as a whole + identifier, not a parseable owner or kind. + """ + provider_kind: str + """Descriptive provider family that produced this row; not a routing key.""" + + source: ModelProviderProvenanceSource + """Kind of component that supplied the adapter.""" - tenant_id: str - """Tenant id or tenant selector, such as common or organizations.""" + owner_display_name: str | None = None + """Human-readable contributor name, not the adapter display name.""" - access_token_to_renew: str | None = None - """Previously rejected token that OneAuth must bypass during renewal.""" + owner_id: str | None = None + """Stable contributor identifier when the adapter has an owner outside the runtime. + Independent of the contribution mechanism and not a routing key. + """ @staticmethod - def from_dict(obj: Any) -> 'EntraTokenAcquireRequest': + def from_dict(obj: Any) -> 'ModelProviderProvenance': assert isinstance(obj, dict) - client_id = from_str(obj.get("clientId")) - interaction = EntraTokenInteraction(obj.get("interaction")) - redirect_uri = from_str(obj.get("redirectUri")) - scopes = from_list(from_str, obj.get("scopes")) - tenant_id = from_str(obj.get("tenantId")) - access_token_to_renew = from_union([from_str, from_none], obj.get("accessTokenToRenew")) - return EntraTokenAcquireRequest(client_id, interaction, redirect_uri, scopes, tenant_id, access_token_to_renew) + adapter_id = from_str(obj.get("adapterId")) + provider_kind = from_str(obj.get("providerKind")) + source = ModelProviderProvenanceSource(obj.get("source")) + owner_display_name = from_union([from_str, from_none], obj.get("ownerDisplayName")) + owner_id = from_union([from_str, from_none], obj.get("ownerId")) + return ModelProviderProvenance(adapter_id, provider_kind, source, owner_display_name, owner_id) def to_dict(self) -> dict: result: dict = {} - result["clientId"] = from_str(self.client_id) - result["interaction"] = to_enum(EntraTokenInteraction, self.interaction) - result["redirectUri"] = from_str(self.redirect_uri) - result["scopes"] = from_list(from_str, self.scopes) - result["tenantId"] = from_str(self.tenant_id) - if self.access_token_to_renew is not None: - result["accessTokenToRenew"] = from_union([from_str, from_none], self.access_token_to_renew) + result["adapterId"] = from_str(self.adapter_id) + result["providerKind"] = from_str(self.provider_kind) + result["source"] = to_enum(ModelProviderProvenanceSource, self.source) + if self.owner_display_name is not None: + result["ownerDisplayName"] = from_union([from_str, from_none], self.owner_display_name) + if self.owner_id is not None: + result["ownerId"] = from_union([from_str, from_none], self.owner_id) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class EntraTokenAcquireResult: - """Result of a OneAuth token acquisition.""" +class ModelProviderAttribution: + """Attribution for the adapter itself. - status: EntraTokenAcquireResultStatus - """OneAuth token acquisition outcome discriminator.""" + Contributor attribution, independent of routing identity and authorization. + """ + source: ModelProviderProvenanceSource + """Kind of component that supplied the adapter. Attribution does not confer authority.""" - access_token: str | None = None - """Opaque access token.""" + owner_display_name: str | None = None + """Human-readable contributor name, not the adapter display name.""" - account_id: str | None = None - """Opaque OneAuth account id, when supplied by the broker.""" + owner_id: str | None = None + """Stable contributor identifier. Required and nonblank for extension and custom sources; + optional for built-in and configured sources. Does not grant authority. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderAttribution': + assert isinstance(obj, dict) + source = ModelProviderProvenanceSource(obj.get("source")) + owner_display_name = from_union([from_str, from_none], obj.get("ownerDisplayName")) + owner_id = from_union([from_str, from_none], obj.get("ownerId")) + return ModelProviderAttribution(source, owner_display_name, owner_id) - expires_on_timestamp: float | None = None - """Expiry as milliseconds since Unix epoch, when supplied by OneAuth.""" + def to_dict(self) -> dict: + result: dict = {} + result["source"] = to_enum(ModelProviderProvenanceSource, self.source) + if self.owner_display_name is not None: + result["ownerDisplayName"] = from_union([from_str, from_none], self.owner_display_name) + if self.owner_id is not None: + result["ownerId"] = from_union([from_str, from_none], self.owner_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderOperationOutcome: + """Typed operation outcome. + + Typed provider-operation outcome. Use the code for control flow and the optional message + for display. + + Typed operation outcome. Passive discovery can return `absent` with an empty instance + list. + """ + code: ModelProviderOperationOutcomeCode + """Machine-readable operation outcome.""" + + message: str | None = None + """Human-readable detail for non-success outcomes.""" @staticmethod - def from_dict(obj: Any) -> 'EntraTokenAcquireResult': + def from_dict(obj: Any) -> 'ModelProviderOperationOutcome': assert isinstance(obj, dict) - status = EntraTokenAcquireResultStatus(obj.get("status")) - access_token = from_union([from_str, from_none], obj.get("accessToken")) - account_id = from_union([from_str, from_none], obj.get("accountId")) - expires_on_timestamp = from_union([from_float, from_none], obj.get("expiresOnTimestamp")) - return EntraTokenAcquireResult(status, access_token, account_id, expires_on_timestamp) + code = ModelProviderOperationOutcomeCode(obj.get("code")) + message = from_union([from_str, from_none], obj.get("message")) + return ModelProviderOperationOutcome(code, message) def to_dict(self) -> dict: result: dict = {} - result["status"] = to_enum(EntraTokenAcquireResultStatus, self.status) - if self.access_token is not None: - result["accessToken"] = from_union([from_str, from_none], self.access_token) - if self.account_id is not None: - result["accountId"] = from_union([from_str, from_none], self.account_id) - if self.expires_on_timestamp is not None: - result["expiresOnTimestamp"] = from_union([to_float, from_none], self.expires_on_timestamp) + result["code"] = to_enum(ModelProviderOperationOutcomeCode, self.code) + if self.message is not None: + result["message"] = from_union([from_str, from_none], self.message) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class EnvironmentsListRequest: + """Optional discovery filters supported by GitHub Mission Control.""" + + kind: EnvironmentKind | None = None + """Restrict discovery to this compute kind.""" + + status: str | None = None + """Operational status, such as online, offline, degraded, waking, or draining.""" + + @staticmethod + def from_dict(obj: Any) -> 'EnvironmentsListRequest': + assert isinstance(obj, dict) + kind = from_union([EnvironmentKind, from_none], obj.get("kind")) + status = from_union([from_str, from_none], obj.get("status")) + return EnvironmentsListRequest(kind, status) + + def to_dict(self) -> dict: + result: dict = {} + if self.kind is not None: + result["kind"] = from_union([lambda x: to_enum(EnvironmentKind, x), from_none], self.kind) + if self.status is not None: + result["status"] = from_union([from_str, from_none], self.status) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -20537,6 +21703,49 @@ def to_dict(self) -> dict: ExternalToolTextResultForLlmContentResourceDetails = EmbeddedTextResourceContents | EmbeddedBlobResourceContents +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPPromptIcon: + """An MCP prompt icon with standard size hints and preserved non-standard fields.""" + + src: str + """Icon URI""" + + additional_properties: dict[str, Any] | None = None + """Server-provided non-standard icon fields""" + + mime_type: str | None = None + """Icon MIME type, when known""" + + sizes: list[str] | None = None + """Icon sizes, such as `48x48` or `any`""" + + theme: str | None = None + """Theme hint for this icon""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPPromptIcon': + assert isinstance(obj, dict) + src = from_str(obj.get("src")) + additional_properties = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("additionalProperties")) + mime_type = from_union([from_str, from_none], obj.get("mimeType")) + sizes = from_union([lambda x: from_list(from_str, x), from_none], obj.get("sizes")) + theme = from_union([from_str, from_none], obj.get("theme")) + return MCPPromptIcon(src, additional_properties, mime_type, sizes, theme) + + def to_dict(self) -> dict: + result: dict = {} + result["src"] = from_str(self.src) + if self.additional_properties is not None: + result["additionalProperties"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.additional_properties) + if self.mime_type is not None: + result["mimeType"] = from_union([from_str, from_none], self.mime_type) + if self.sizes is not None: + result["sizes"] = from_union([lambda x: from_list(from_str, x), from_none], self.sizes) + if self.theme is not None: + result["theme"] = from_union([from_str, from_none], self.theme) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPResourceIcon: @@ -21046,6 +22255,42 @@ def to_dict(self) -> dict: result["mode"] = to_enum(HistoryRewindMode, self.mode) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class HostConfiguration: + """Normalized listener settings delivered only to the supervised hosting participant.""" + + resume_factory: bool + """Whether app-owned durable sessions are resumed by the owning application.""" + + session_factory: bool + """Whether session materialization is delegated to the owning application.""" + + github_environment: HostGitHubEnvironmentOptions | None = None + """Requested GitHub Mission Control registration.""" + + local_server: HostLocalServerConfiguration | None = None + """Normalized local listener settings, absent for relay-only hosts.""" + + @staticmethod + def from_dict(obj: Any) -> 'HostConfiguration': + assert isinstance(obj, dict) + resume_factory = from_bool(obj.get("resumeFactory")) + session_factory = from_bool(obj.get("sessionFactory")) + github_environment = from_union([HostGitHubEnvironmentOptions.from_dict, from_none], obj.get("githubEnvironment")) + local_server = from_union([HostLocalServerConfiguration.from_dict, from_none], obj.get("localServer")) + return HostConfiguration(resume_factory, session_factory, github_environment, local_server) + + def to_dict(self) -> dict: + result: dict = {} + result["resumeFactory"] = from_bool(self.resume_factory) + result["sessionFactory"] = from_bool(self.session_factory) + if self.github_environment is not None: + result["githubEnvironment"] = from_union([lambda x: to_class(HostGitHubEnvironmentOptions, x), from_none], self.github_environment) + if self.local_server is not None: + result["localServer"] = from_union([lambda x: to_class(HostLocalServerConfiguration, x), from_none], self.local_server) + return result + @dataclass class HostExitedNotification: """Reports a supervised listener's hosting-task termination and cleanup outcome.""" @@ -21081,6 +22326,49 @@ def to_dict(self) -> dict: result["exitCode"] = from_union([from_int, from_none], self.exit_code) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class HostStartRequest: + """Starts a supervised AHP host with at least one explicitly selected transport.""" + + host_id: str + """Caller-generated UUID identifying this connection-owned listener.""" + + github_environment: HostGitHubEnvironmentOptions | None = None + """Registers a GitHub Mission Control environment and enables its relay transport.""" + + local_server: HostLocalServerOptions | None = None + """Enables a local WebSocket listener.""" + + resume_factory: bool | None = None + """Ask the owning application to resume its durable AHP sessions.""" + + session_factory: bool | None = None + """Ask the owning SDK application to materialize AHP sessions.""" + + @staticmethod + def from_dict(obj: Any) -> 'HostStartRequest': + assert isinstance(obj, dict) + host_id = from_str(obj.get("hostId")) + github_environment = from_union([HostGitHubEnvironmentOptions.from_dict, from_none], obj.get("githubEnvironment")) + local_server = from_union([HostLocalServerOptions.from_dict, from_none], obj.get("localServer")) + resume_factory = from_union([from_bool, from_none], obj.get("resumeFactory")) + session_factory = from_union([from_bool, from_none], obj.get("sessionFactory")) + return HostStartRequest(host_id, github_environment, local_server, resume_factory, session_factory) + + def to_dict(self) -> dict: + result: dict = {} + result["hostId"] = from_str(self.host_id) + if self.github_environment is not None: + result["githubEnvironment"] = from_union([lambda x: to_class(HostGitHubEnvironmentOptions, x), from_none], self.github_environment) + if self.local_server is not None: + result["localServer"] = from_union([lambda x: to_class(HostLocalServerOptions, x), from_none], self.local_server) + if self.resume_factory is not None: + result["resumeFactory"] = from_union([from_bool, from_none], self.resume_factory) + if self.session_factory is not None: + result["sessionFactory"] = from_union([from_bool, from_none], self.session_factory) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPAppsReadResourceRequest: @@ -22231,6 +23519,95 @@ def to_dict(self) -> dict: result["url"] = from_union([from_str, from_none], self.url) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ManagedSettingsComposeLayer: + """One candidate channel; absent settings represents a channel that delivered no document.""" + + source: ManagedSettingsChannel + """The channel whose candidate document is being supplied.""" + + settings: Any = None + """Candidate managed-settings document. Omit when the channel delivered none, as in resolve + output. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ManagedSettingsComposeLayer': + assert isinstance(obj, dict) + source = ManagedSettingsChannel(obj.get("source")) + settings = obj.get("settings") + return ManagedSettingsComposeLayer(source, settings) + + def to_dict(self) -> dict: + result: dict = {} + result["source"] = to_enum(ManagedSettingsChannel, self.source) + if self.settings is not None: + result["settings"] = self.settings + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ManagedSettingsDiagnostic: + """One validation finding for a managed-settings document.""" + + message: str + """Human-readable description of the finding.""" + + path: str + """Dot-separated path of the offending setting, such as `autoTier.overridable`. Empty for + the document as a whole. + """ + severity: ManagedSettingsDiagnosticSeverity + """Whether the finding rejects the document.""" + + @staticmethod + def from_dict(obj: Any) -> 'ManagedSettingsDiagnostic': + assert isinstance(obj, dict) + message = from_str(obj.get("message")) + path = from_str(obj.get("path")) + severity = ManagedSettingsDiagnosticSeverity(obj.get("severity")) + return ManagedSettingsDiagnostic(message, path, severity) + + def to_dict(self) -> dict: + result: dict = {} + result["message"] = from_str(self.message) + result["path"] = from_str(self.path) + result["severity"] = to_enum(ManagedSettingsDiagnosticSeverity, self.severity) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ManagedSettingsMeta: + """Per-key lock state and provenance for `values`. + + Per-key lock state and provenance for `ManagedSettingsValues`, with the same field names. + Producers emit each typed key in values and meta together; both outer objects are omitted + when no typed key is set. + + Per-key lock state and provenance for the entries in `values`, using the same key names. + """ + auto_tier: ManagedSettingMeta | None = None + """Lock state and provenance of `values.autoTier`.""" + + model: ManagedSettingMeta | None = None + """Lock state and provenance of `values.model`.""" + + @staticmethod + def from_dict(obj: Any) -> 'ManagedSettingsMeta': + assert isinstance(obj, dict) + auto_tier = from_union([ManagedSettingMeta.from_dict, from_none], obj.get("autoTier")) + model = from_union([ManagedSettingMeta.from_dict, from_none], obj.get("model")) + return ManagedSettingsMeta(auto_tier, model) + + def to_dict(self) -> dict: + result: dict = {} + if self.auto_tier is not None: + result["autoTier"] = from_union([lambda x: to_class(ManagedSettingMeta, x), from_none], self.auto_tier) + if self.model is not None: + result["model"] = from_union([lambda x: to_class(ManagedSettingMeta, x), from_none], self.model) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MarketplaceListResult: @@ -22409,7 +23786,7 @@ def to_dict(self) -> dict: @dataclass class MCPOauthLoginRequest: """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. """ server_name: str """Name of the remote MCP server to authenticate""" @@ -22456,6 +23833,13 @@ class MCPOauthLoginRequest: runtime treats it as confidential and uses the per-login clientSecret if provided, otherwise retrieving the client secret from the MCP OAuth secret store. """ + redirect_uri: str | None = field(default=None, kw_only=True) + """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. + """ @staticmethod def from_dict(obj: Any) -> 'MCPOauthLoginRequest': @@ -22470,7 +23854,8 @@ def from_dict(obj: Any) -> 'MCPOauthLoginRequest': grant_type = from_union([MCPGrantType, from_none], obj.get("grantType")) login_id = from_union([from_str, from_none], obj.get("loginId")) public_client = from_union([from_bool, from_none], obj.get("publicClient")) - return MCPOauthLoginRequest(server_name, callback_success_message, client_id, client_name, client_secret, force_reauth, grant_type, public_client, login_id=login_id, expected_installation_id=expected_installation_id) + redirect_uri = from_union([from_str, from_none], obj.get("redirectUri")) + return MCPOauthLoginRequest(server_name, callback_success_message, client_id, client_name, client_secret, force_reauth, grant_type, public_client, redirect_uri=redirect_uri, login_id=login_id, expected_installation_id=expected_installation_id) def to_dict(self) -> dict: result: dict = {} @@ -22493,6 +23878,8 @@ def to_dict(self) -> dict: result["loginId"] = from_union([from_str, from_none], self.login_id) if self.public_client is not None: result["publicClient"] = from_union([from_bool, from_none], self.public_client) + if self.redirect_uri is not None: + result["redirectUri"] = from_union([from_str, from_none], self.redirect_uri) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -22675,12 +24062,18 @@ class MCPOauthLoginResult: """OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server. """ + authorization_id: str | None = None + """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. + """ authorization_url: str | None = None """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. """ login_id: str | None = None """Runtime-issued owned flow identity; never a server name or installation operation ID.""" @@ -22691,13 +24084,16 @@ class MCPOauthLoginResult: @staticmethod def from_dict(obj: Any) -> 'MCPOauthLoginResult': assert isinstance(obj, dict) + authorization_id = from_union([from_str, from_none], obj.get("authorizationId")) authorization_url = from_union([from_str, from_none], obj.get("authorizationUrl")) login_id = from_union([from_str, from_none], obj.get("loginId")) status = from_union([MCPOwnedOauthLoginStatus, from_none], obj.get("status")) - return MCPOauthLoginResult(authorization_url, login_id, status) + return MCPOauthLoginResult(authorization_id, authorization_url, login_id, status) def to_dict(self) -> dict: result: dict = {} + if self.authorization_id is not None: + result["authorizationId"] = from_union([from_str, from_none], self.authorization_id) if self.authorization_url is not None: result["authorizationUrl"] = from_union([from_str, from_none], self.authorization_url) if self.login_id is not None: @@ -22990,6 +24386,43 @@ def to_dict(self) -> dict: result["title"] = from_union([from_str, from_none], self.title) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPPromptMessage: + """An MCP prompt message with opaque JSON content preserved without flattening or + content-type filtering. + """ + content: Any + """The original MCP content block, including nested metadata and unfamiliar content types""" + + role: MCPPromptRole + """The role of the message sender""" + + meta: dict[str, Any] | None = None + """Message-level metadata""" + + additional_properties: dict[str, Any] | None = None + """Server-provided non-standard message fields""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPPromptMessage': + assert isinstance(obj, dict) + content = obj.get("content") + role = MCPPromptRole(obj.get("role")) + meta = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("_meta")) + additional_properties = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("additionalProperties")) + return MCPPromptMessage(content, role, meta, additional_properties) + + def to_dict(self) -> dict: + result: dict = {} + result["content"] = self.content + result["role"] = to_enum(MCPPromptRole, self.role) + if self.meta is not None: + result["_meta"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.meta) + if self.additional_properties is not None: + result["additionalProperties"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.additional_properties) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPSamplingExecutionResult: @@ -23536,44 +24969,6 @@ def to_dict(self) -> dict: result["outputPrice"] = from_union([to_float, from_none], self.output_price) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ModelCapabilitiesLimits: - """Token limits for prompts, outputs, and context window""" - - max_context_window_tokens: int | None = None - """Maximum total context window size in tokens""" - - max_output_tokens: int | None = None - """Maximum number of output/completion tokens""" - - max_prompt_tokens: int | None = None - """Maximum number of prompt/input tokens""" - - vision: ModelCapabilitiesLimitsVision | None = None - """Vision-specific limits""" - - @staticmethod - def from_dict(obj: Any) -> 'ModelCapabilitiesLimits': - assert isinstance(obj, dict) - max_context_window_tokens = from_union([from_int, from_none], obj.get("max_context_window_tokens")) - max_output_tokens = from_union([from_int, from_none], obj.get("max_output_tokens")) - max_prompt_tokens = from_union([from_int, from_none], obj.get("max_prompt_tokens")) - vision = from_union([ModelCapabilitiesLimitsVision.from_dict, from_none], obj.get("vision")) - return ModelCapabilitiesLimits(max_context_window_tokens, max_output_tokens, max_prompt_tokens, vision) - - def to_dict(self) -> dict: - result: dict = {} - if self.max_context_window_tokens is not None: - result["max_context_window_tokens"] = from_union([from_int, from_none], self.max_context_window_tokens) - if self.max_output_tokens is not None: - result["max_output_tokens"] = from_union([from_int, from_none], self.max_output_tokens) - if self.max_prompt_tokens is not None: - result["max_prompt_tokens"] = from_union([from_int, from_none], self.max_prompt_tokens) - if self.vision is not None: - result["vision"] = from_union([lambda x: to_class(ModelCapabilitiesLimitsVision, x), from_none], self.vision) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionModelPriceCategory: @@ -23663,55 +25058,76 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class ModelSwitchAutoTierResult: - """Immediate acknowledgement and Auto preference snapshot after a switch request. This - result never implies that a pending preference committed. +class ModelProviderAutomaticDiscoveryPolicy: + """Adapter-declared policy for passive and automatic discovery. + + Adapter-declared policy that tells clients whether discovery may run automatically. """ - status: ModelSwitchAutoTierStatus - """Immediate request status. `pending` means accepted but not committed.""" + mode: ModelProviderAutomaticDiscoveryMode + """Whether automatic discovery is allowed, limited to configured providers, or explicit-only.""" - activating_auto_tier: AutoTier | None = None - """Auto preference currently claimed by an in-progress activation. Null means the activation - is returning to provider-default routing. + network_scope: ModelProviderDiscoveryNetworkScope + """Maximum network scope used by this adapter during discovery.""" + + requires_input: bool + """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. + """ + requires_trust: bool + """True when the adapter must be enabled by a trusted owner, such as a trusted extension, + before automatic discovery may run. """ - effective_auto_tier: AutoTier | None = None - """Auto preference currently committed for the session.""" - pending_auto_tier: AutoTier | None = None - """Latest unclaimed Auto preference waiting for a future user turn.""" + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderAutomaticDiscoveryPolicy': + assert isinstance(obj, dict) + mode = ModelProviderAutomaticDiscoveryMode(obj.get("mode")) + network_scope = ModelProviderDiscoveryNetworkScope(obj.get("networkScope")) + requires_input = from_bool(obj.get("requiresInput")) + requires_trust = from_bool(obj.get("requiresTrust")) + return ModelProviderAutomaticDiscoveryPolicy(mode, network_scope, requires_input, requires_trust) - superseded_auto_tier: AutoTier | None = None - """Earlier unclaimed preference replaced by this request. This can be present with either - status, including when selecting the effective preference cancels pending work. + def to_dict(self) -> dict: + result: dict = {} + result["mode"] = to_enum(ModelProviderAutomaticDiscoveryMode, self.mode) + result["networkScope"] = to_enum(ModelProviderDiscoveryNetworkScope, self.network_scope) + result["requiresInput"] = from_bool(self.requires_input) + result["requiresTrust"] = from_bool(self.requires_trust) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ProtocolSectionOverride: + action: ProtocolSectionOverrideAction + """Declarative operation applied to the section. + + Section override action discriminator. """ + content: str | None = None + """Optional content used by replace, append, and prepend operations.""" @staticmethod - def from_dict(obj: Any) -> 'ModelSwitchAutoTierResult': + def from_dict(obj: Any) -> 'ProtocolSectionOverride': assert isinstance(obj, dict) - status = ModelSwitchAutoTierStatus(obj.get("status")) - activating_auto_tier = from_union([AutoTier, from_none], obj.get("activatingAutoTier")) - effective_auto_tier = from_union([AutoTier, from_none], obj.get("effectiveAutoTier")) - pending_auto_tier = from_union([AutoTier, from_none], obj.get("pendingAutoTier")) - superseded_auto_tier = from_union([AutoTier, from_none], obj.get("supersededAutoTier")) - return ModelSwitchAutoTierResult(status, activating_auto_tier, effective_auto_tier, pending_auto_tier, superseded_auto_tier) + action = ProtocolSectionOverrideAction(obj.get("action")) + content = from_union([from_str, from_none], obj.get("content")) + return ProtocolSectionOverride(action, content) def to_dict(self) -> dict: result: dict = {} - result["status"] = to_enum(ModelSwitchAutoTierStatus, self.status) - if self.activating_auto_tier is not None: - result["activatingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.activating_auto_tier) - if self.effective_auto_tier is not None: - result["effectiveAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.effective_auto_tier) - if self.pending_auto_tier is not None: - result["pendingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.pending_auto_tier) - if self.superseded_auto_tier is not None: - result["supersededAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.superseded_auto_tier) + result["action"] = to_enum(ProtocolSectionOverrideAction, self.action) + if self.content is not None: + result["content"] = from_union([from_str, from_none], self.content) return result # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class NamedProviderConfig: - """External SDK input for a named custom model provider. Ingested by the native protocol + """Provider connection prepared from the instance's inference metadata. Carries no + credential; supply one if the endpoint requires it. + + External SDK input for a named custom model provider. Ingested by the native protocol boundary before host dispatch. """ base_url: str @@ -23735,6 +25151,10 @@ class NamedProviderConfig: headers: dict[str, str] | None = None """Additional HTTP headers included with provider requests.""" + model_provider: ProviderConfigModelProvider | None = None + """The product serving the provider's models, reported in telemetry as `model_provider`. + Only affects telemetry. + """ transport: ProviderTransport | None = None """Transport used to communicate with the provider.""" @@ -23754,10 +25174,11 @@ def from_dict(obj: Any) -> 'NamedProviderConfig': bearer_token = from_union([from_str, from_none], obj.get("bearerToken")) has_bearer_token_provider = from_union([from_bool, from_none], obj.get("hasBearerTokenProvider")) headers = from_union([lambda x: from_dict(from_str, x), from_none], obj.get("headers")) + model_provider = from_union([ProviderConfigModelProvider, from_none], obj.get("modelProvider")) transport = from_union([ProviderTransport, from_none], obj.get("transport")) type = from_union([ProviderType, from_none], obj.get("type")) wire_api = from_union([ProviderWireAPI, from_none], obj.get("wireApi")) - return NamedProviderConfig(base_url, name, api_key, azure, bearer_token, has_bearer_token_provider, headers, transport, type, wire_api) + return NamedProviderConfig(base_url, name, api_key, azure, bearer_token, has_bearer_token_provider, headers, model_provider, transport, type, wire_api) def to_dict(self) -> dict: result: dict = {} @@ -23773,6 +25194,8 @@ def to_dict(self) -> dict: result["hasBearerTokenProvider"] = from_union([from_bool, from_none], self.has_bearer_token_provider) if self.headers is not None: result["headers"] = from_union([lambda x: from_dict(from_str, x), from_none], self.headers) + if self.model_provider is not None: + result["modelProvider"] = from_union([lambda x: to_enum(ProviderConfigModelProvider, x), from_none], self.model_provider) if self.transport is not None: result["transport"] = from_union([lambda x: to_enum(ProviderTransport, x), from_none], self.transport) if self.type is not None: @@ -23781,6 +25204,91 @@ def to_dict(self) -> dict: result["wireApi"] = from_union([lambda x: to_enum(ProviderWireAPI, x), from_none], self.wire_api) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderGetStatusRequest: + """Provider status request parameters.""" + + instance: ModelProviderInstanceReference + """Provider instance reference returned by discovery.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderGetStatusRequest': + assert isinstance(obj, dict) + instance = ModelProviderInstanceReference.from_dict(obj.get("instance")) + return ModelProviderGetStatusRequest(instance) + + def to_dict(self) -> dict: + result: dict = {} + result["instance"] = to_class(ModelProviderInstanceReference, self.instance) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderModelsListRequest: + """Provider model inventory request parameters.""" + + instance: ModelProviderInstanceReference + """Provider instance reference returned by discovery.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderModelsListRequest': + assert isinstance(obj, dict) + instance = ModelProviderInstanceReference.from_dict(obj.get("instance")) + return ModelProviderModelsListRequest(instance) + + def to_dict(self) -> dict: + result: dict = {} + result["instance"] = to_class(ModelProviderInstanceReference, self.instance) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelSwitchAutoTierResult: + """Immediate acknowledgement and Auto preference snapshot after a switch request. This + result never implies that a pending preference committed. + """ + status: ModelSwitchAutoTierStatus + """Immediate request status. `pending` means accepted but not committed.""" + + activating_auto_tier: AutoTier | None = None + """Auto preference currently claimed by an in-progress activation. Null means the activation + is returning to provider-default routing. + """ + effective_auto_tier: AutoTier | None = None + """Auto preference currently committed for the session.""" + + pending_auto_tier: AutoTier | None = None + """Latest unclaimed Auto preference waiting for a future user turn.""" + + superseded_auto_tier: AutoTier | None = None + """Earlier unclaimed preference replaced by this request. This can be present with either + status, including when selecting the effective preference cancels pending work. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelSwitchAutoTierResult': + assert isinstance(obj, dict) + status = ModelSwitchAutoTierStatus(obj.get("status")) + activating_auto_tier = from_union([AutoTier, from_none], obj.get("activatingAutoTier")) + effective_auto_tier = from_union([AutoTier, from_none], obj.get("effectiveAutoTier")) + pending_auto_tier = from_union([AutoTier, from_none], obj.get("pendingAutoTier")) + superseded_auto_tier = from_union([AutoTier, from_none], obj.get("supersededAutoTier")) + return ModelSwitchAutoTierResult(status, activating_auto_tier, effective_auto_tier, pending_auto_tier, superseded_auto_tier) + + def to_dict(self) -> dict: + result: dict = {} + result["status"] = to_enum(ModelSwitchAutoTierStatus, self.status) + if self.activating_auto_tier is not None: + result["activatingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.activating_auto_tier) + if self.effective_auto_tier is not None: + result["effectiveAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.effective_auto_tier) + if self.pending_auto_tier is not None: + result["pendingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.pending_auto_tier) + if self.superseded_auto_tier is not None: + result["supersededAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.superseded_auto_tier) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class OptionsUpdateAdditionalContentExclusionPolicyRule: @@ -25167,6 +26675,63 @@ def to_dict(self) -> dict: result["description"] = from_union([from_str, from_none], self.description) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPConfiguredServer: + """Effective MCP configuration entry. Configuration enablement is distinct from the optional + live observation. + """ + enabled: bool + """Whether this configured server is enabled after session configuration and policy + filtering. + """ + name: str + """Server name (config key)""" + + display_name: str | None = None + """Human-readable display name supplied by configuration.""" + + live: MCPConfiguredServerState | None = None + """Observed state from an already materialized matching server. Omitted when no live graph + has this configured server; it never determines configuration enablement. + """ + source: McpServerSource | None = None + """Configuration provenance: user, workspace, plugin, builtin, or managed.""" + + source_plugin: str | None = None + """Plugin name that provided this server, when source is plugin.""" + + source_plugin_version: str | None = None + """Plugin version that provided this server, when source is plugin.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPConfiguredServer': + assert isinstance(obj, dict) + enabled = from_bool(obj.get("enabled")) + name = from_str(obj.get("name")) + display_name = from_union([from_str, from_none], obj.get("displayName")) + live = from_union([MCPConfiguredServerState.from_dict, from_none], obj.get("live")) + source = from_union([McpServerSource, from_none], obj.get("source")) + source_plugin = from_union([from_str, from_none], obj.get("sourcePlugin")) + source_plugin_version = from_union([from_str, from_none], obj.get("sourcePluginVersion")) + return MCPConfiguredServer(enabled, name, display_name, live, source, source_plugin, source_plugin_version) + + def to_dict(self) -> dict: + result: dict = {} + result["enabled"] = from_bool(self.enabled) + result["name"] = from_str(self.name) + if self.display_name is not None: + result["displayName"] = from_union([from_str, from_none], self.display_name) + if self.live is not None: + result["live"] = from_union([lambda x: to_class(MCPConfiguredServerState, x), from_none], self.live) + if self.source is not None: + result["source"] = from_union([lambda x: to_enum(McpServerSource, x), from_none], self.source) + if self.source_plugin is not None: + result["sourcePlugin"] = from_union([from_str, from_none], self.source_plugin) + if self.source_plugin_version is not None: + result["sourcePluginVersion"] = from_union([from_str, from_none], self.source_plugin_version) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPServer: @@ -25756,27 +27321,30 @@ class UISessionLimitsExhaustedResponseAction(Enum): # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class ProtocolSectionOverride: - action: ProtocolSectionOverrideAction - """Declarative operation applied to the section. +class ProtocolSystemMessageReplaceConfig: + content: str + """Complete replacement system-message text.""" - Section override action discriminator. - """ - content: str | None = None - """Optional content used by replace, append, and prepend operations.""" + mode: ProtocolReplaceMode + """Replace-mode discriminator.""" + + content_blocks: list[SystemMessageBlock] | None = None + """Optional structured blocks corresponding to the replacement content.""" @staticmethod - def from_dict(obj: Any) -> 'ProtocolSectionOverride': + def from_dict(obj: Any) -> 'ProtocolSystemMessageReplaceConfig': assert isinstance(obj, dict) - action = ProtocolSectionOverrideAction(obj.get("action")) - content = from_union([from_str, from_none], obj.get("content")) - return ProtocolSectionOverride(action, content) + content = from_str(obj.get("content")) + mode = ProtocolReplaceMode(obj.get("mode")) + content_blocks = from_union([lambda x: from_list(SystemMessageBlock.from_dict, x), from_none], obj.get("contentBlocks")) + return ProtocolSystemMessageReplaceConfig(content, mode, content_blocks) def to_dict(self) -> dict: result: dict = {} - result["action"] = to_enum(ProtocolSectionOverrideAction, self.action) - if self.content is not None: - result["content"] = from_union([from_str, from_none], self.content) + result["content"] = from_str(self.content) + result["mode"] = to_enum(ProtocolReplaceMode, self.mode) + if self.content_blocks is not None: + result["contentBlocks"] = from_union([lambda x: from_list(lambda x: to_class(SystemMessageBlock, x), x), from_none], self.content_blocks) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -25802,34 +27370,6 @@ def to_dict(self) -> dict: result["content"] = from_union([from_str, from_none], self.content) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ProtocolSystemMessageReplaceConfig: - content: str - """Complete replacement system-message text.""" - - mode: ProtocolReplaceMode - """Replace-mode discriminator.""" - - content_blocks: list[SystemMessageBlock] | None = None - """Optional structured blocks corresponding to the replacement content.""" - - @staticmethod - def from_dict(obj: Any) -> 'ProtocolSystemMessageReplaceConfig': - assert isinstance(obj, dict) - content = from_str(obj.get("content")) - mode = ProtocolReplaceMode(obj.get("mode")) - content_blocks = from_union([lambda x: from_list(SystemMessageBlock.from_dict, x), from_none], obj.get("contentBlocks")) - return ProtocolSystemMessageReplaceConfig(content, mode, content_blocks) - - def to_dict(self) -> dict: - result: dict = {} - result["content"] = from_str(self.content) - result["mode"] = to_enum(ProtocolReplaceMode, self.mode) - if self.content_blocks is not None: - result["contentBlocks"] = from_union([lambda x: from_list(lambda x: to_class(SystemMessageBlock, x), x), from_none], self.content_blocks) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ProviderEndpoint: @@ -26923,6 +28463,41 @@ def to_dict(self) -> dict: result["reason"] = from_union([from_str, from_none], self.reason) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SandboxProxyCAStatus: + """Status of the persistent certificate authority of the sandbox credential proxy.""" + + can_install: bool + """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. + """ + state: SandboxProxyCAState + """The state of the certificate authority.""" + + detail: str | None = None + """Human-readable reason for the state. On `installed` or `notInstalled`, present only when + the certificate authority must be rotated, and then says why. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SandboxProxyCAStatus': + assert isinstance(obj, dict) + can_install = from_bool(obj.get("canInstall")) + state = SandboxProxyCAState(obj.get("state")) + detail = from_union([from_str, from_none], obj.get("detail")) + return SandboxProxyCAStatus(can_install, state, detail) + + def to_dict(self) -> dict: + result: dict = {} + result["canInstall"] = from_bool(self.can_install) + result["state"] = to_enum(SandboxProxyCAState, self.state) + if self.detail is not None: + result["detail"] = from_union([from_str, from_none], self.detail) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SlashCommandTextResult: @@ -27077,18 +28652,26 @@ class SessionFSError: message: str | None = None """Free-form detail about the error, for logging/diagnostics""" + write_changed: bool | None = None + """For failed writeFile requests only: true if the provider changed the target before + failing. Omit when unknown or unchanged. + """ + @staticmethod def from_dict(obj: Any) -> 'SessionFSError': assert isinstance(obj, dict) code = SessionFSErrorCode(obj.get("code")) message = from_union([from_str, from_none], obj.get("message")) - return SessionFSError(code, message) + write_changed = from_union([from_bool, from_none], obj.get("writeChanged")) + return SessionFSError(code, message, write_changed) def to_dict(self) -> dict: result: dict = {} result["code"] = to_enum(SessionFSErrorCode, self.code) if self.message is not None: result["message"] = from_union([from_str, from_none], self.message) + if self.write_changed is not None: + result["writeChanged"] = from_union([from_bool, from_none], self.write_changed) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -29832,6 +31415,30 @@ def to_dict(self) -> dict: result["saved"] = from_union([lambda x: to_class(Saved, x), from_none], self.saved) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ConnectorSessionAccount: + """Session account selection.""" + + account_id: str + """Opaque session-scoped account selection ID.""" + + auth_info: AuthIdentityMetadata + """Credential-free identity metadata.""" + + @staticmethod + def from_dict(obj: Any) -> 'ConnectorSessionAccount': + assert isinstance(obj, dict) + account_id = from_str(obj.get("accountId")) + auth_info = AuthIdentityMetadata.from_dict(obj.get("authInfo")) + return ConnectorSessionAccount(account_id, auth_info) + + def to_dict(self) -> dict: + result: dict = {} + result["accountId"] = from_str(self.account_id) + result["authInfo"] = to_class(AuthIdentityMetadata, self.auth_info) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AuthReadValue: @@ -30874,52 +32481,41 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class AuthLoginStep: - """The first step of the flow. +class AuthLoginResultDto: + """Login result. When status is needs-plaintext-consent or needs-account-selection, advance + with the user's decision to continue. - 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). + Result of an interactive login flow. Pending consent or account selection is not terminal. """ - kind: AuthLoginStepKind - """Login flow step variant discriminator.""" - - url: str | None = None - """Authorize URL the consumer should open in a browser (consumer-driven browser-open).""" - - prompt: str | None = None - """Prompt for the value the provider needs; the consumer supplies it as advance input (e.g. - a GitHub Enterprise Cloud host, *.ghe.com). + status: AuthLoginResultStatus + """Current disposition of the login, including pending user decisions.""" + host: str | None = None + """Host that was signed in, when completed.""" + login: str | None = None + """Login that was signed in, when completed.""" + accounts: list[AuthLoginAccount] | None = None + """Available accounts when sign-in is awaiting account selection, ordered with Microsoft 365 + first. """ - result: AuthLoginResultDto | None = None - """The terminal login result.""" - - message: str | None = None - """Human-readable failure message.""" @staticmethod - def from_dict(obj: Any) -> 'AuthLoginStep': + def from_dict(obj: Any) -> 'AuthLoginResultDto': assert isinstance(obj, dict) - kind = AuthLoginStepKind(obj.get("kind")) - url = from_union([from_str, from_none], obj.get("url")) - prompt = from_union([from_str, from_none], obj.get("prompt")) - result = from_union([AuthLoginResultDto.from_dict, from_none], obj.get("result")) - message = from_union([from_str, from_none], obj.get("message")) - return AuthLoginStep(kind, url, prompt, result, message) + status = AuthLoginResultStatus(obj.get("status")) + accounts = from_union([lambda x: from_list(AuthLoginAccount.from_dict, x), from_none], obj.get("accounts")) + host = from_union([from_str, from_none], obj.get("host")) + login = from_union([from_str, from_none], obj.get("login")) + return AuthLoginResultDto(status, host, login, accounts) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(AuthLoginStepKind, self.kind) - if self.url is not None: - result["url"] = from_union([from_str, from_none], self.url) - if self.prompt is not None: - result["prompt"] = from_union([from_str, from_none], self.prompt) - if self.result is not None: - result["result"] = from_union([lambda x: to_class(AuthLoginResultDto, x), from_none], self.result) - if self.message is not None: - result["message"] = from_union([from_str, from_none], self.message) + result["status"] = to_enum(AuthLoginResultStatus, self.status) + if self.accounts is not None: + result["accounts"] = from_union([lambda x: from_list(lambda x: to_class(AuthLoginAccount, x), x), from_none], self.accounts) + if self.host is not None: + result["host"] = from_union([from_str, from_none], self.host) + if self.login is not None: + result["login"] = from_union([from_str, from_none], self.login) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -31122,6 +32718,110 @@ def to_dict(self) -> dict: result["trust"] = from_union([lambda x: (x).to_dict(), from_none], self.trust) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class GitHubEnvironment: + """The requested environment without relay bootstrap credentials. + + Safe discovery information. Host-side relay bootstrap credentials are never included. + """ + id: str + """Identifier assigned by Mission Control.""" + + kind: EnvironmentKind + """Compute kind reported by Mission Control.""" + + name: str + """Human-readable environment name.""" + + status: str + """Open-ended operational status vocabulary.""" + + capabilities: EnvironmentCapabilities | None = None + """Hosting capabilities advertised by the environment.""" + + labels: dict[str, str] | None = None + """Discovery labels attached to the environment.""" + + last_heartbeat_at: str | None = None + """Timestamp of the last heartbeat received by Mission Control.""" + + org_id: str | None = None + """Organization identifier, when the environment belongs to an organization.""" + + owner_id: str | None = None + """Identifier of the environment owner.""" + + owner_type: str | None = None + """Owner category reported by Mission Control.""" + + @staticmethod + def from_dict(obj: Any) -> 'GitHubEnvironment': + assert isinstance(obj, dict) + id = from_str(obj.get("id")) + kind = EnvironmentKind(obj.get("kind")) + name = from_str(obj.get("name")) + status = from_str(obj.get("status")) + capabilities = from_union([EnvironmentCapabilities.from_dict, from_none], obj.get("capabilities")) + labels = from_union([lambda x: from_dict(from_str, x), from_none], obj.get("labels")) + last_heartbeat_at = from_union([from_str, from_none], obj.get("lastHeartbeatAt")) + org_id = from_union([from_str, from_none], obj.get("orgId")) + owner_id = from_union([from_str, from_none], obj.get("ownerId")) + owner_type = from_union([from_str, from_none], obj.get("ownerType")) + return GitHubEnvironment(id, kind, name, status, capabilities, labels, last_heartbeat_at, org_id, owner_id, owner_type) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(self.id) + result["kind"] = to_enum(EnvironmentKind, self.kind) + result["name"] = from_str(self.name) + result["status"] = from_str(self.status) + if self.capabilities is not None: + result["capabilities"] = from_union([lambda x: to_class(EnvironmentCapabilities, x), from_none], self.capabilities) + if self.labels is not None: + result["labels"] = from_union([lambda x: from_dict(from_str, x), from_none], self.labels) + if self.last_heartbeat_at is not None: + result["lastHeartbeatAt"] = from_union([from_str, from_none], self.last_heartbeat_at) + if self.org_id is not None: + result["orgId"] = from_union([from_str, from_none], self.org_id) + if self.owner_id is not None: + result["ownerId"] = from_union([from_str, from_none], self.owner_id) + if self.owner_type is not None: + result["ownerType"] = from_union([from_str, from_none], self.owner_type) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class CustomizationsReloadResult: + """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. + """ + errors: list[str] + """Errors from any component that could not be refreshed""" + + outcomes: list[CustomizationReloadOutcome] + """Outcome of each component in reload order; a skipped component was not configured or + loaded + """ + warnings: list[str] + """Warnings from skill discovery""" + + @staticmethod + def from_dict(obj: Any) -> 'CustomizationsReloadResult': + assert isinstance(obj, dict) + errors = from_list(from_str, obj.get("errors")) + outcomes = from_list(CustomizationReloadOutcome.from_dict, obj.get("outcomes")) + warnings = from_list(from_str, obj.get("warnings")) + return CustomizationsReloadResult(errors, outcomes, warnings) + + def to_dict(self) -> dict: + result: dict = {} + result["errors"] = from_list(from_str, self.errors) + result["outcomes"] = from_list(lambda x: to_class(CustomizationReloadOutcome, x), self.outcomes) + result["warnings"] = from_list(from_str, self.warnings) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class DebugCollectLogsResult: @@ -31303,6 +33003,64 @@ def to_dict(self) -> dict: result["warnings"] = from_list(from_str, self.warnings) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderInstance: + """A normalized model-provider instance discovered by the runtime. + + The discovered instance that serves the model. + + Normalized provider instance. + """ + display_name: str + """Human-readable instance name.""" + + provenance: ModelProviderProvenance + """Attribution for the adapter that produced this instance.""" + + reference: ModelProviderInstanceReference + """Self-contained reference for subsequent provider operations.""" + + inference_endpoint: str | None = None + """Inference API endpoint when the provider exposes one separately from its management + endpoint. + """ + inference_transport: ProviderTransport | None = None + """Transport to use for inference against this instance.""" + + inference_type: ProviderType | None = None + """Provider family to use for inference against this instance.""" + + inference_wire_api: ProviderWireAPI | None = None + """Wire API to use for inference against this instance, when required by the provider family.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderInstance': + assert isinstance(obj, dict) + display_name = from_str(obj.get("displayName")) + provenance = ModelProviderProvenance.from_dict(obj.get("provenance")) + reference = ModelProviderInstanceReference.from_dict(obj.get("reference")) + inference_endpoint = from_union([from_str, from_none], obj.get("inferenceEndpoint")) + inference_transport = from_union([ProviderTransport, from_none], obj.get("inferenceTransport")) + inference_type = from_union([ProviderType, from_none], obj.get("inferenceType")) + inference_wire_api = from_union([ProviderWireAPI, from_none], obj.get("inferenceWireApi")) + return ModelProviderInstance(display_name, provenance, reference, inference_endpoint, inference_transport, inference_type, inference_wire_api) + + def to_dict(self) -> dict: + result: dict = {} + result["displayName"] = from_str(self.display_name) + result["provenance"] = to_class(ModelProviderProvenance, self.provenance) + result["reference"] = to_class(ModelProviderInstanceReference, self.reference) + if self.inference_endpoint is not None: + result["inferenceEndpoint"] = from_union([from_str, from_none], self.inference_endpoint) + if self.inference_transport is not None: + result["inferenceTransport"] = from_union([lambda x: to_enum(ProviderTransport, x), from_none], self.inference_transport) + if self.inference_type is not None: + result["inferenceType"] = from_union([lambda x: to_enum(ProviderType, x), from_none], self.inference_type) + if self.inference_wire_api is not None: + result["inferenceWireApi"] = from_union([lambda x: to_enum(ProviderWireAPI, x), from_none], self.inference_wire_api) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ExtensionList: @@ -31724,6 +33482,62 @@ def to_dict(self) -> dict: result["title"] = from_union([from_str, from_none], self.title) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPPrompt: + """An MCP prompt descriptor. Server-provided non-standard fields are exposed under + `additionalProperties`. + """ + name: str + """The programmatic name of the prompt""" + + meta: dict[str, Any] | None = None + """Prompt-level metadata""" + + additional_properties: dict[str, Any] | None = None + """Server-provided non-standard descriptor fields""" + + arguments: list[MCPPromptArgument] | None = None + """Arguments accepted by the prompt""" + + description: str | None = None + """Description of what this prompt provides""" + + icons: list[MCPPromptIcon] | None = None + """Icons associated with this prompt""" + + title: str | None = None + """Human-readable display title""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPPrompt': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + meta = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("_meta")) + additional_properties = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("additionalProperties")) + arguments = from_union([lambda x: from_list(MCPPromptArgument.from_dict, x), from_none], obj.get("arguments")) + description = from_union([from_str, from_none], obj.get("description")) + icons = from_union([lambda x: from_list(MCPPromptIcon.from_dict, x), from_none], obj.get("icons")) + title = from_union([from_str, from_none], obj.get("title")) + return MCPPrompt(name, meta, additional_properties, arguments, description, icons, title) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + if self.meta is not None: + result["_meta"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.meta) + if self.additional_properties is not None: + result["additionalProperties"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.additional_properties) + if self.arguments is not None: + result["arguments"] = from_union([lambda x: from_list(lambda x: to_class(MCPPromptArgument, x), x), from_none], self.arguments) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + if self.icons is not None: + result["icons"] = from_union([lambda x: from_list(lambda x: to_class(MCPPromptIcon, x), x), from_none], self.icons) + if self.title is not None: + result["title"] = from_union([from_str, from_none], self.title) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPOauthHandlePendingRequest: @@ -32581,6 +34395,167 @@ def to_dict(self) -> dict: result["name"] = from_union([from_str, from_none], self.name) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ManagedSettingsComposeRequest: + """Candidate managed-settings documents to merge without applying them.""" + + layers: list[ManagedSettingsComposeLayer] + """One entry per channel. `source` must be `device`, `server`, or `policyHelper`, each at + most once (checked at runtime); order does not matter, because channel precedence is + fixed. To preview documents from resolve output, map recognized source strings to + ManagedSettingsChannel and copy their settings; generated resolve and compose layer types + are distinct. Omitted settings means this channel delivered no document. Supplied + documents must be valid within the preview limits; warnings are returned in diagnostics. + Compose does not reproduce source-failure state or retained enforcement floors from + resolve. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ManagedSettingsComposeRequest': + assert isinstance(obj, dict) + layers = from_list(ManagedSettingsComposeLayer.from_dict, obj.get("layers")) + return ManagedSettingsComposeRequest(layers) + + def to_dict(self) -> dict: + result: dict = {} + result["layers"] = from_list(lambda x: to_class(ManagedSettingsComposeLayer, x), self.layers) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ManagedSettingsValidateResult: + """Result of validating a managed-settings document.""" + + diagnostics: list[ManagedSettingsDiagnostic] + """Errors that reject the document and warnings about content the runtime ignores.""" + + valid: bool + """Whether the runtime would accept the document within the preview resource limits. Always + equals whether `settings` is present. An invalid document is rejected as a whole. + """ + settings: Any = None + """Canonical form of the document the runtime would apply, with unrecognized keys removed. + Absent when the document is invalid. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ManagedSettingsValidateResult': + assert isinstance(obj, dict) + diagnostics = from_list(ManagedSettingsDiagnostic.from_dict, obj.get("diagnostics")) + valid = from_bool(obj.get("valid")) + settings = obj.get("settings") + return ManagedSettingsValidateResult(diagnostics, valid, settings) + + def to_dict(self) -> dict: + result: dict = {} + result["diagnostics"] = from_list(lambda x: to_class(ManagedSettingsDiagnostic, x), self.diagnostics) + result["valid"] = from_bool(self.valid) + if self.settings is not None: + result["settings"] = self.settings + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ManagedSettingsComposeResult: + """The effective managed settings the runtime would enforce for the given documents, in the + same shape `managedSettings.resolve` returns. + """ + diagnostics: list[ManagedSettingsDiagnostic] + """Warnings about ignored content, with paths prefixed by the channel name.""" + + layers: list[ManagedSettingsLayer] + """Only the supplied channels, strongest first, with canonical documents. Empty canonical + documents are represented as absent settings, as in live resolution. + """ + resolved: ManagedSettingsResolvedData + """Effective managed settings, in the same shape as `session.managedSettings.get`.""" + + meta: ManagedSettingsMeta | None = None + """Per-key lock state and provenance for `values`.""" + + values: ManagedSettingsValues | None = None + """Typed effective values, as in `managedSettings.resolve`.""" + + @staticmethod + def from_dict(obj: Any) -> 'ManagedSettingsComposeResult': + assert isinstance(obj, dict) + diagnostics = from_list(ManagedSettingsDiagnostic.from_dict, obj.get("diagnostics")) + layers = from_list(ManagedSettingsLayer.from_dict, obj.get("layers")) + resolved = ManagedSettingsResolvedData.from_dict(obj.get("resolved")) + meta = from_union([ManagedSettingsMeta.from_dict, from_none], obj.get("meta")) + values = from_union([ManagedSettingsValues.from_dict, from_none], obj.get("values")) + return ManagedSettingsComposeResult(diagnostics, layers, resolved, meta, values) + + def to_dict(self) -> dict: + result: dict = {} + result["diagnostics"] = from_list(lambda x: to_class(ManagedSettingsDiagnostic, x), self.diagnostics) + result["layers"] = from_list(lambda x: to_class(ManagedSettingsLayer, x), self.layers) + result["resolved"] = to_class(ManagedSettingsResolvedData, self.resolved) + if self.meta is not None: + result["meta"] = from_union([lambda x: to_class(ManagedSettingsMeta, x), from_none], self.meta) + if self.values is not None: + result["values"] = from_union([lambda x: to_class(ManagedSettingsValues, x), from_none], self.values) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ManagedSettingsResolveResult: + """Effective enterprise managed settings for an account, resolved without a session.""" + + diagnostics: list[ManagedSettingsDiagnostic] + """Warnings about unavailable policy sources or a failed refresh served from cache. A cached + response is not proof of a successful live fetch; `resolved.failClosed` separately + describes enforcement. + """ + layers: list[ManagedSettingsLayer] + """Each managed-settings channel consulted, strongest first, with the validated document it + delivered before merging. `resolved.settings` is the merged result. More channels may be + added over time. + """ + resolved: ManagedSettingsResolvedData + """Effective managed settings from the device and account (server) channels, in the same + shape as `session.managedSettings.get`, excluding session-local injection. + """ + account: str | None = None + """Printable opaque identity of the account the settings were resolved for, suitable for + comparison and storage, not an account selectionId. Absent when no account was available, + in which case only device policy is reported. + """ + meta: ManagedSettingsMeta | None = None + """Per-key lock state and provenance for the entries in `values`, using the same key names.""" + + values: ManagedSettingsValues | None = None + """Typed effective values of managed settings, keyed like the managed-settings schema and + already resolved across channels, with the `{ "overridable": ... }` wrapper removed. + Present when policy sets at least one typed key. Keys not typed here are available in + `resolved.settings`. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ManagedSettingsResolveResult': + assert isinstance(obj, dict) + diagnostics = from_list(ManagedSettingsDiagnostic.from_dict, obj.get("diagnostics")) + layers = from_list(ManagedSettingsLayer.from_dict, obj.get("layers")) + resolved = ManagedSettingsResolvedData.from_dict(obj.get("resolved")) + account = from_union([from_str, from_none], obj.get("account")) + meta = from_union([ManagedSettingsMeta.from_dict, from_none], obj.get("meta")) + values = from_union([ManagedSettingsValues.from_dict, from_none], obj.get("values")) + return ManagedSettingsResolveResult(diagnostics, layers, resolved, account, meta, values) + + def to_dict(self) -> dict: + result: dict = {} + result["diagnostics"] = from_list(lambda x: to_class(ManagedSettingsDiagnostic, x), self.diagnostics) + result["layers"] = from_list(lambda x: to_class(ManagedSettingsLayer, x), self.layers) + result["resolved"] = to_class(ManagedSettingsResolvedData, self.resolved) + if self.account is not None: + result["account"] = from_union([from_str, from_none], self.account) + if self.meta is not None: + result["meta"] = from_union([lambda x: to_class(ManagedSettingsMeta, x), from_none], self.meta) + if self.values is not None: + result["values"] = from_union([lambda x: to_class(ManagedSettingsValues, x), from_none], self.values) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPAppsHostContext: @@ -32784,6 +34759,43 @@ def to_dict(self) -> dict: result["restoresPreviousConfiguration"] = from_bool(self.restores_previous_configuration) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPPromptsGetResult: + """Prompt messages returned by the MCP server without sending them to the model.""" + + messages: list[MCPPromptMessage] + """Ordered prompt messages""" + + meta: dict[str, Any] | None = None + """MCP result metadata""" + + additional_properties: dict[str, Any] | None = None + """Server-provided non-standard result fields""" + + description: str | None = None + """Description of the prompt""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPPromptsGetResult': + assert isinstance(obj, dict) + messages = from_list(MCPPromptMessage.from_dict, obj.get("messages")) + meta = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("_meta")) + additional_properties = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("additionalProperties")) + description = from_union([from_str, from_none], obj.get("description")) + return MCPPromptsGetResult(messages, meta, additional_properties, description) + + def to_dict(self) -> dict: + result: dict = {} + result["messages"] = from_list(lambda x: to_class(MCPPromptMessage, x), self.messages) + if self.meta is not None: + result["_meta"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.meta) + if self.additional_properties is not None: + result["additionalProperties"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.additional_properties) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class DebugCollectLogsEntry: @@ -32994,6 +35006,133 @@ def to_dict(self) -> dict: result["supports"] = from_union([lambda x: to_class(ModelCapabilitiesOverrideSupports, x), from_none], self.supports) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderAdapterDescriptor: + """A normalized model-provider adapter in the session's effective catalog.""" + + adapter_id: str + """Stable opaque identity for routing to this adapter. Unique in the effective catalog, + independent of live registration generations. + """ + automatic_discovery: ModelProviderAutomaticDiscoveryPolicy + """Adapter-declared policy for passive and automatic discovery.""" + + display_name: str + """Human-readable provider name.""" + + operations: list[ModelProviderAdapterOperationDescriptor] + """Operations supported by this provider adapter.""" + + provenance: ModelProviderAttribution + """Attribution for the adapter itself.""" + + provider_kind: str + """Descriptive provider family, such as `ollama`. Different adapters may have the same + family; use adapterId for routing. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderAdapterDescriptor': + assert isinstance(obj, dict) + adapter_id = from_str(obj.get("adapterId")) + automatic_discovery = ModelProviderAutomaticDiscoveryPolicy.from_dict(obj.get("automaticDiscovery")) + display_name = from_str(obj.get("displayName")) + operations = from_list(ModelProviderAdapterOperationDescriptor.from_dict, obj.get("operations")) + provenance = ModelProviderAttribution.from_dict(obj.get("provenance")) + provider_kind = from_str(obj.get("providerKind")) + return ModelProviderAdapterDescriptor(adapter_id, automatic_discovery, display_name, operations, provenance, provider_kind) + + def to_dict(self) -> dict: + result: dict = {} + result["adapterId"] = from_str(self.adapter_id) + result["automaticDiscovery"] = to_class(ModelProviderAutomaticDiscoveryPolicy, self.automatic_discovery) + result["displayName"] = from_str(self.display_name) + result["operations"] = from_list(lambda x: to_class(ModelProviderAdapterOperationDescriptor, x), self.operations) + result["provenance"] = to_class(ModelProviderAttribution, self.provenance) + result["providerKind"] = from_str(self.provider_kind) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ProtocolSystemMessageConfig: + """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. + """ + content: str | None = None + """Text appended to the standard system prompt. + + Complete replacement system-message text. + + Text appended after the customized sections. + """ + mode: ProtocolMode | None = None + """Append-mode discriminator. Omission also selects append mode. + + Replace-mode discriminator. + + Customize-mode discriminator. + """ + content_blocks: list[SystemMessageBlock] | None = None + """Optional structured blocks corresponding to the replacement content.""" + + sections: dict[str, ProtocolSectionOverride] | None = None + """Named standard-prompt section overrides.""" + + @staticmethod + def from_dict(obj: Any) -> 'ProtocolSystemMessageConfig': + assert isinstance(obj, dict) + content = from_union([from_str, from_none], obj.get("content")) + mode = from_union([ProtocolMode, from_none], obj.get("mode")) + content_blocks = from_union([lambda x: from_list(SystemMessageBlock.from_dict, x), from_none], obj.get("contentBlocks")) + sections = from_union([lambda x: from_dict(ProtocolSectionOverride.from_dict, x), from_none], obj.get("sections")) + return ProtocolSystemMessageConfig(content, mode, content_blocks, sections) + + def to_dict(self) -> dict: + result: dict = {} + if self.content is not None: + result["content"] = from_union([from_str, from_none], self.content) + if self.mode is not None: + result["mode"] = from_union([lambda x: to_enum(ProtocolMode, x), from_none], self.mode) + if self.content_blocks is not None: + result["contentBlocks"] = from_union([lambda x: from_list(lambda x: to_class(SystemMessageBlock, x), x), from_none], self.content_blocks) + if self.sections is not None: + result["sections"] = from_union([lambda x: from_dict(lambda x: to_class(ProtocolSectionOverride, x), x), from_none], self.sections) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ProtocolSystemMessageCustomizeConfig: + mode: ProtocolCustomizeMode + """Customize-mode discriminator.""" + + content: str | None = None + """Text appended after the customized sections.""" + + sections: dict[str, ProtocolSectionOverride] | None = None + """Named standard-prompt section overrides.""" + + @staticmethod + def from_dict(obj: Any) -> 'ProtocolSystemMessageCustomizeConfig': + assert isinstance(obj, dict) + mode = ProtocolCustomizeMode(obj.get("mode")) + content = from_union([from_str, from_none], obj.get("content")) + sections = from_union([lambda x: from_dict(ProtocolSectionOverride.from_dict, x), from_none], obj.get("sections")) + return ProtocolSystemMessageCustomizeConfig(mode, content, sections) + + def to_dict(self) -> dict: + result: dict = {} + result["mode"] = to_enum(ProtocolCustomizeMode, self.mode) + if self.content is not None: + result["content"] = from_union([from_str, from_none], self.content) + if self.sections is not None: + result["sections"] = from_union([lambda x: from_dict(lambda x: to_class(ProtocolSectionOverride, x), x), from_none], self.sections) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class OptionsUpdateAdditionalContentExclusionPolicy: @@ -33422,6 +35561,26 @@ def to_dict(self) -> dict: result["plugins"] = from_list(lambda x: to_class(MarketplacePluginInfo, x), self.plugins) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPConfiguredServerList: + """Effective MCP configuration with optional live observations from matching already + materialized servers. + """ + servers: list[MCPConfiguredServer] + """Effective configured MCP servers.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPConfiguredServerList': + assert isinstance(obj, dict) + servers = from_list(MCPConfiguredServer.from_dict, obj.get("servers")) + return MCPConfiguredServerList(servers) + + def to_dict(self) -> dict: + result: dict = {} + result["servers"] = from_list(lambda x: to_class(MCPConfiguredServer, x), self.servers) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPServerList: @@ -33591,86 +35750,6 @@ def to_dict(self) -> dict: result["maxAiCredits"] = from_union([to_float, from_none], self.max_ai_credits) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ProtocolSystemMessageConfig: - """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. - """ - content: str | None = None - """Text appended to the standard system prompt. - - Complete replacement system-message text. - - Text appended after the customized sections. - """ - mode: ProtocolMode | None = None - """Append-mode discriminator. Omission also selects append mode. - - Replace-mode discriminator. - - Customize-mode discriminator. - """ - content_blocks: list[SystemMessageBlock] | None = None - """Optional structured blocks corresponding to the replacement content.""" - - sections: dict[str, ProtocolSectionOverride] | None = None - """Named standard-prompt section overrides.""" - - @staticmethod - def from_dict(obj: Any) -> 'ProtocolSystemMessageConfig': - assert isinstance(obj, dict) - content = from_union([from_str, from_none], obj.get("content")) - mode = from_union([ProtocolMode, from_none], obj.get("mode")) - content_blocks = from_union([lambda x: from_list(SystemMessageBlock.from_dict, x), from_none], obj.get("contentBlocks")) - sections = from_union([lambda x: from_dict(ProtocolSectionOverride.from_dict, x), from_none], obj.get("sections")) - return ProtocolSystemMessageConfig(content, mode, content_blocks, sections) - - def to_dict(self) -> dict: - result: dict = {} - if self.content is not None: - result["content"] = from_union([from_str, from_none], self.content) - if self.mode is not None: - result["mode"] = from_union([lambda x: to_enum(ProtocolMode, x), from_none], self.mode) - if self.content_blocks is not None: - result["contentBlocks"] = from_union([lambda x: from_list(lambda x: to_class(SystemMessageBlock, x), x), from_none], self.content_blocks) - if self.sections is not None: - result["sections"] = from_union([lambda x: from_dict(lambda x: to_class(ProtocolSectionOverride, x), x), from_none], self.sections) - return result - -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ProtocolSystemMessageCustomizeConfig: - mode: ProtocolCustomizeMode - """Customize-mode discriminator.""" - - content: str | None = None - """Text appended after the customized sections.""" - - sections: dict[str, ProtocolSectionOverride] | None = None - """Named standard-prompt section overrides.""" - - @staticmethod - def from_dict(obj: Any) -> 'ProtocolSystemMessageCustomizeConfig': - assert isinstance(obj, dict) - mode = ProtocolCustomizeMode(obj.get("mode")) - content = from_union([from_str, from_none], obj.get("content")) - sections = from_union([lambda x: from_dict(ProtocolSectionOverride.from_dict, x), from_none], obj.get("sections")) - return ProtocolSystemMessageCustomizeConfig(mode, content, sections) - - def to_dict(self) -> dict: - result: dict = {} - result["mode"] = to_enum(ProtocolCustomizeMode, self.mode) - if self.content is not None: - result["content"] = from_union([from_str, from_none], self.content) - if self.sections is not None: - result["sections"] = from_union([lambda x: from_dict(lambda x: to_class(ProtocolSectionOverride, x), x), from_none], self.sections) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PushAttachmentGitHubFileDiff: @@ -34161,6 +36240,31 @@ def to_dict(self) -> dict: result["seatbelt"] = from_union([lambda x: to_class(SandboxConfigUserPolicySeatbelt, x), from_none], self.seatbelt) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionFSReadFileBytesResult: + """File bytes as standard base64, or a filesystem error if the read failed.""" + + content: str + """Exact file bytes encoded as standard base64""" + + error: SessionFSError | None = None + """Describes a filesystem error.""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionFSReadFileBytesResult': + assert isinstance(obj, dict) + content = from_str(obj.get("content")) + error = from_union([SessionFSError.from_dict, from_none], obj.get("error")) + return SessionFSReadFileBytesResult(content, error) + + def to_dict(self) -> dict: + result: dict = {} + result["content"] = from_str(self.content) + if self.error is not None: + result["error"] = from_union([lambda x: to_class(SessionFSError, x), from_none], self.error) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionFSReadFileResult: @@ -36911,26 +39015,53 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass -class AuthLoginBegun: - """A started login flow: its opaque id and first step.""" +class AuthLoginStep: + """The first step of the flow. - flow_id: str - """Opaque flow id used to advance or cancel this login.""" + 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). + """ + kind: AuthLoginStepKind + """Login flow step variant discriminator.""" - step: AuthLoginStep - """The first step of the flow.""" + url: str | None = None + """Authorize URL the consumer should open in a browser (consumer-driven browser-open).""" + + prompt: str | None = None + """Prompt for the value the provider needs; the consumer supplies it as advance input (e.g. + a GitHub Enterprise Cloud host, *.ghe.com). + """ + result: AuthLoginResultDto | None = None + """Login result. When status is needs-plaintext-consent or needs-account-selection, advance + with the user's decision to continue. + """ + message: str | None = None + """Human-readable failure message.""" @staticmethod - def from_dict(obj: Any) -> 'AuthLoginBegun': + def from_dict(obj: Any) -> 'AuthLoginStep': assert isinstance(obj, dict) - flow_id = from_str(obj.get("flowId")) - step = AuthLoginStep.from_dict(obj.get("step")) - return AuthLoginBegun(flow_id, step) + kind = AuthLoginStepKind(obj.get("kind")) + url = from_union([from_str, from_none], obj.get("url")) + prompt = from_union([from_str, from_none], obj.get("prompt")) + result = from_union([AuthLoginResultDto.from_dict, from_none], obj.get("result")) + message = from_union([from_str, from_none], obj.get("message")) + return AuthLoginStep(kind, url, prompt, result, message) def to_dict(self) -> dict: result: dict = {} - result["flowId"] = from_str(self.flow_id) - result["step"] = to_class(AuthLoginStep, self.step) + result["kind"] = to_enum(AuthLoginStepKind, self.kind) + if self.url is not None: + result["url"] = from_union([from_str, from_none], self.url) + if self.prompt is not None: + result["prompt"] = from_union([from_str, from_none], self.prompt) + if self.result is not None: + result["result"] = from_union([lambda x: to_class(AuthLoginResultDto, x), from_none], self.result) + if self.message is not None: + result["message"] = from_union([from_str, from_none], self.message) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -37088,6 +39219,44 @@ def to_dict(self) -> dict: result["session"] = from_union([lambda x: to_class(CanvasSessionContext, x), from_none], self.session) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class EnvironmentsGetResult: + """Safe discovery information for the requested environment.""" + + environment: GitHubEnvironment + """The requested environment without relay bootstrap credentials.""" + + @staticmethod + def from_dict(obj: Any) -> 'EnvironmentsGetResult': + assert isinstance(obj, dict) + environment = GitHubEnvironment.from_dict(obj.get("environment")) + return EnvironmentsGetResult(environment) + + def to_dict(self) -> dict: + result: dict = {} + result["environment"] = to_class(GitHubEnvironment, self.environment) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class EnvironmentsListResult: + """Environments visible to the authenticated caller and matching the supplied filters.""" + + environments: list[GitHubEnvironment] + """Safe discovery records without relay bootstrap credentials.""" + + @staticmethod + def from_dict(obj: Any) -> 'EnvironmentsListResult': + assert isinstance(obj, dict) + environments = from_list(GitHubEnvironment.from_dict, obj.get("environments")) + return EnvironmentsListResult(environments) + + def to_dict(self) -> dict: + result: dict = {} + result["environments"] = from_list(lambda x: to_class(GitHubEnvironment, x), self.environments) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class DiagnosticsConfiguration: @@ -37126,6 +39295,68 @@ def to_dict(self) -> dict: result["sources"] = to_class(DiagnosticSourcesConfiguration, self.sources) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderDiscoverResult: + """Provider instances found by a discovery operation.""" + + instances: list[ModelProviderInstance] + """Discovered provider instances. Empty when passive default discovery finds no reachable + provider. + """ + outcome: ModelProviderOperationOutcome + """Typed operation outcome. Passive discovery can return `absent` with an empty instance + list. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderDiscoverResult': + assert isinstance(obj, dict) + instances = from_list(ModelProviderInstance.from_dict, obj.get("instances")) + outcome = ModelProviderOperationOutcome.from_dict(obj.get("outcome")) + return ModelProviderDiscoverResult(instances, outcome) + + def to_dict(self) -> dict: + result: dict = {} + result["instances"] = from_list(lambda x: to_class(ModelProviderInstance, x), self.instances) + result["outcome"] = to_class(ModelProviderOperationOutcome, self.outcome) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderStatus: + """Current health information for a provider instance.""" + + instance: ModelProviderInstance + """Normalized provider instance.""" + + outcome: ModelProviderOperationOutcome + """Typed operation outcome.""" + + status: str + """Open provider status value, such as `healthy`, `unreachable`, or `notInstalled`.""" + + version: str | None = None + """Provider-reported version.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderStatus': + assert isinstance(obj, dict) + instance = ModelProviderInstance.from_dict(obj.get("instance")) + outcome = ModelProviderOperationOutcome.from_dict(obj.get("outcome")) + status = from_str(obj.get("status")) + version = from_union([from_str, from_none], obj.get("version")) + return ModelProviderStatus(instance, outcome, status, version) + + def to_dict(self) -> dict: + result: dict = {} + result["instance"] = to_class(ModelProviderInstance, self.instance) + result["outcome"] = to_class(ModelProviderOperationOutcome, self.outcome) + result["status"] = from_str(self.status) + if self.version is not None: + result["version"] = from_union([from_str, from_none], self.version) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class HandlePendingToolCallRequest: @@ -37183,6 +39414,43 @@ def to_dict(self) -> dict: result["toolArgs"] = self.tool_args return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPPromptsListResult: + """One page of prompts advertised by the named MCP server.""" + + prompts: list[MCPPrompt] + """Prompts advertised by the server""" + + meta: dict[str, Any] | None = None + """MCP result metadata""" + + additional_properties: dict[str, Any] | None = None + """Server-provided non-standard result fields""" + + next_cursor: str | None = None + """Opaque cursor for the next page, if the server has more prompts""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPPromptsListResult': + assert isinstance(obj, dict) + prompts = from_list(MCPPrompt.from_dict, obj.get("prompts")) + meta = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("_meta")) + additional_properties = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("additionalProperties")) + next_cursor = from_union([from_str, from_none], obj.get("nextCursor")) + return MCPPromptsListResult(prompts, meta, additional_properties, next_cursor) + + def to_dict(self) -> dict: + result: dict = {} + result["prompts"] = from_list(lambda x: to_class(MCPPrompt, x), self.prompts) + if self.meta is not None: + result["_meta"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.meta) + if self.additional_properties is not None: + result["additionalProperties"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.additional_properties) + if self.next_cursor is not None: + result["nextCursor"] = from_union([from_str, from_none], self.next_cursor) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class WorkflowRunTerminal: @@ -37905,6 +40173,11 @@ class ProviderConfig: """Well-known model ID used for capability lookup. When set, agent behavior config and token limits are inferred from this model. """ + model_provider: ProviderConfigModelProvider | None = None + """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. + """ provider_name: str | None = None """Provider name used for model and telemetry attribution.""" @@ -37936,12 +40209,13 @@ def from_dict(obj: Any) -> 'ProviderConfig': max_prompt_tokens = from_union([from_float, from_none], obj.get("maxPromptTokens")) model_capabilities = from_union([ModelCapabilitiesOverride.from_dict, from_none], obj.get("modelCapabilities")) model_id = from_union([from_str, from_none], obj.get("modelId")) + model_provider = from_union([ProviderConfigModelProvider, from_none], obj.get("modelProvider")) provider_name = from_union([from_str, from_none], obj.get("providerName")) transport = from_union([ProviderTransport, from_none], obj.get("transport")) type = from_union([ProviderType, from_none], obj.get("type")) wire_api = from_union([ProviderWireAPI, from_none], obj.get("wireApi")) wire_model = from_union([from_str, from_none], obj.get("wireModel")) - return ProviderConfig(base_url, api_key, azure, bearer_token, has_bearer_token_provider, headers, max_context_window_tokens, max_output_tokens, max_prompt_tokens, model_capabilities, model_id, provider_name, transport, type, wire_api, wire_model) + return ProviderConfig(base_url, api_key, azure, bearer_token, has_bearer_token_provider, headers, max_context_window_tokens, max_output_tokens, max_prompt_tokens, model_capabilities, model_id, model_provider, provider_name, transport, type, wire_api, wire_model) def to_dict(self) -> dict: result: dict = {} @@ -37966,6 +40240,8 @@ def to_dict(self) -> dict: result["modelCapabilities"] = from_union([lambda x: to_class(ModelCapabilitiesOverride, x), from_none], self.model_capabilities) if self.model_id is not None: result["modelId"] = from_union([from_str, from_none], self.model_id) + if self.model_provider is not None: + result["modelProvider"] = from_union([lambda x: to_enum(ProviderConfigModelProvider, x), from_none], self.model_provider) if self.provider_name is not None: result["providerName"] = from_union([from_str, from_none], self.provider_name) if self.transport is not None: @@ -37978,6 +40254,26 @@ def to_dict(self) -> dict: result["wireModel"] = from_union([from_str, from_none], self.wire_model) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderAdapterCatalog: + """Normalized model-provider adapter definitions available to the session, not discovered + instances. + """ + providers: list[ModelProviderAdapterDescriptor] + """Available provider adapters ordered by adapterId.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderAdapterCatalog': + assert isinstance(obj, dict) + providers = from_list(ModelProviderAdapterDescriptor.from_dict, obj.get("providers")) + return ModelProviderAdapterCatalog(providers) + + def to_dict(self) -> dict: + result: dict = {} + result["providers"] = from_list(lambda x: to_class(ModelProviderAdapterDescriptor, x), self.providers) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PermissionsConfigureParams: @@ -38363,8 +40659,11 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SandboxConfig: - """Resolved sandbox configuration.""" + """Resolved sandbox configuration. + 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. + """ enabled: bool """Whether sandboxing is enabled for the session.""" @@ -39300,6 +41599,30 @@ def to_dict(self) -> dict: result["status"] = to_class(ConnectorStatus, self.status) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AuthLoginBegun: + """A started login flow: its opaque id and first step.""" + + flow_id: str + """Opaque flow id used to advance or cancel this login.""" + + step: AuthLoginStep + """The first step of the flow.""" + + @staticmethod + def from_dict(obj: Any) -> 'AuthLoginBegun': + assert isinstance(obj, dict) + flow_id = from_str(obj.get("flowId")) + step = AuthLoginStep.from_dict(obj.get("step")) + return AuthLoginBegun(flow_id, step) + + def to_dict(self) -> dict: + result: dict = {} + result["flowId"] = from_str(self.flow_id) + result["step"] = to_class(AuthLoginStep, self.step) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class WorkflowRunSummary: @@ -39564,6 +41887,30 @@ def to_dict(self) -> dict: result["servers"] = from_list(lambda x: to_class(DiscoveredMCPServer, x), self.servers) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SandboxProxyCARequest: + """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. + """ + sandbox_config: SandboxConfig | None = None + """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. + """ + + @staticmethod + def from_dict(obj: Any) -> 'SandboxProxyCARequest': + assert isinstance(obj, dict) + sandbox_config = from_union([SandboxConfig.from_dict, from_none], obj.get("sandboxConfig")) + return SandboxProxyCARequest(sandbox_config) + + def to_dict(self) -> dict: + result: dict = {} + if self.sandbox_config is not None: + result["sandboxConfig"] = from_union([lambda x: to_class(SandboxConfig, x), from_none], self.sandbox_config) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionUpdateOptionsParams: @@ -41385,6 +43732,129 @@ def to_dict(self) -> dict: result["droppedCount"] = from_union([from_int, from_none], self.dropped_count) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelCapabilities: + """Provider-reported model capabilities. Omitted capability fields are unknown; explicit + false values are preserved. + + Model capabilities and limits + """ + limits: ModelCapabilitiesLimits | None = None + """Token limits for prompts, outputs, and context window""" + + supports: ModelCapabilitiesSupports | None = None + """Feature flags indicating what the model supports""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelCapabilities': + assert isinstance(obj, dict) + limits = from_union([ModelCapabilitiesLimits.from_dict, from_none], obj.get("limits")) + supports = from_union([ModelCapabilitiesSupports.from_dict, from_none], obj.get("supports")) + return ModelCapabilities(limits, supports) + + def to_dict(self) -> dict: + result: dict = {} + if self.limits is not None: + result["limits"] = from_union([lambda x: to_class(ModelCapabilitiesLimits, x), from_none], self.limits) + if self.supports is not None: + result["supports"] = from_union([lambda x: to_class(ModelCapabilitiesSupports, x), from_none], self.supports) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class DiscoveredModel: + """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. + + The discovered model to configure. + """ + capabilities: ModelCapabilities + """Provider-reported model capabilities. Omitted capability fields are unknown; explicit + false values are preserved. + """ + details: ModelArtifactDetails + """Provider-reported model artifact details.""" + + id: str + """Provider-native model identifier.""" + + provenance: ModelProviderProvenance + """Attribution for the adapter that produced this model row.""" + + warnings: list[ModelProviderWarning] + """Non-fatal warnings encountered while enriching this model.""" + + digest: str | None = None + """Provider-reported artifact digest.""" + + modified_at: datetime | None = None + """Provider-reported last-modified timestamp.""" + + name: str | None = None + """Provider-reported display name.""" + + size_bytes: int | None = None + """Provider-reported artifact size in bytes.""" + + @staticmethod + def from_dict(obj: Any) -> 'DiscoveredModel': + assert isinstance(obj, dict) + capabilities = ModelCapabilities.from_dict(obj.get("capabilities")) + details = ModelArtifactDetails.from_dict(obj.get("details")) + id = from_str(obj.get("id")) + provenance = ModelProviderProvenance.from_dict(obj.get("provenance")) + warnings = from_list(ModelProviderWarning.from_dict, obj.get("warnings")) + digest = from_union([from_str, from_none], obj.get("digest")) + modified_at = from_union([from_datetime, from_none], obj.get("modifiedAt")) + name = from_union([from_str, from_none], obj.get("name")) + size_bytes = from_union([from_int, from_none], obj.get("sizeBytes")) + return DiscoveredModel(capabilities, details, id, provenance, warnings, digest, modified_at, name, size_bytes) + + def to_dict(self) -> dict: + result: dict = {} + result["capabilities"] = to_class(ModelCapabilities, self.capabilities) + result["details"] = to_class(ModelArtifactDetails, self.details) + result["id"] = from_str(self.id) + result["provenance"] = to_class(ModelProviderProvenance, self.provenance) + result["warnings"] = from_list(lambda x: to_class(ModelProviderWarning, x), self.warnings) + if self.digest is not None: + result["digest"] = from_union([from_str, from_none], self.digest) + if self.modified_at is not None: + result["modifiedAt"] = from_union([lambda x: x.isoformat(), from_none], self.modified_at) + if self.name is not None: + result["name"] = from_union([from_str, from_none], self.name) + if self.size_bytes is not None: + result["sizeBytes"] = from_union([from_int, from_none], self.size_bytes) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class DiscoveredModelList: + """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. + """ + models: list[DiscoveredModel] + """Provider-native models in provider order.""" + + outcome: ModelProviderOperationOutcome + """Typed operation outcome.""" + + @staticmethod + def from_dict(obj: Any) -> 'DiscoveredModelList': + assert isinstance(obj, dict) + models = from_list(DiscoveredModel.from_dict, obj.get("models")) + outcome = ModelProviderOperationOutcome.from_dict(obj.get("outcome")) + return DiscoveredModelList(models, outcome) + + def to_dict(self) -> dict: + result: dict = {} + result["models"] = from_list(lambda x: to_class(DiscoveredModel, x), self.models) + result["outcome"] = to_class(ModelProviderOperationOutcome, self.outcome) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class EnvAuthInfo: @@ -41682,11 +44152,43 @@ def to_dict(self) -> dict: result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ManagedSettingsSchemaResult: + """The authoring JSON schema for managed settings recognized by this runtime.""" + + runtime_version: str + """Version of the runtime that owns this schema.""" + + schema: Any + """JSON schema (draft 2020-12) with descriptive shared `x-composition` annotations, not a + complete runtime composition contract. Model, effortLevel, and contextTier remain + coupled; use `managedSettings.compose` for the runtime's effective result. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ManagedSettingsSchemaResult': + assert isinstance(obj, dict) + runtime_version = from_str(obj.get("runtimeVersion")) + schema = obj.get("schema") + return ManagedSettingsSchemaResult(runtime_version, schema) + + def to_dict(self) -> dict: + result: dict = {} + result["runtimeVersion"] = from_str(self.runtime_version) + result["schema"] = self.schema + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPExecuteSamplingParams: """Identifiers and raw MCP CreateMessageRequest params used to run a sampling inference.""" + mcp_request_id: Any + """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). + """ request: dict[str, Any] """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 @@ -41700,11 +44202,6 @@ class MCPExecuteSamplingParams: server_name: str """Name of the MCP server that initiated the sampling request""" - mcp_request_id: Any = None - """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). - """ @staticmethod def from_dict(obj: Any) -> 'MCPExecuteSamplingParams': assert isinstance(obj, dict) @@ -41801,18 +44298,18 @@ def to_dict(self) -> dict: class MCPRegisterExternalClientRequest: """Registration parameters for an external MCP client.""" - server_name: str - """Logical server name for the external client""" - - client: Any = None + client: Any """In-process MCP Client instance. Marked internal: cannot be serialized across the JSON-RPC boundary. """ - config: Any = None + config: Any """In-process server config (MCPServerConfig) paired with the in-process client/transport. Marked internal alongside its companions. """ - transport: Any = None + server_name: str + """Logical server name for the external client""" + + transport: Any """In-process MCP Transport instance. Marked internal: cannot be serialized across the JSON-RPC boundary. """ @@ -42390,10 +44887,10 @@ def to_dict(self) -> dict: class MCPServerConfigMemory: """In-process MCP server configuration used by embedded SDK clients.""" - type: MCPServerConfigMemoryType - server_instance: Any = None + server_instance: Any """In-process MCP server instance. This value cannot cross a JSON-RPC boundary.""" + type: MCPServerConfigMemoryType config_warnings: list[str] | None = None """Configuration warnings recorded while loading the server.""" @@ -42582,32 +45079,6 @@ def to_dict(self) -> dict: result["modelId"] = from_str(self.model_id) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ModelCapabilities: - """Model capabilities and limits""" - - limits: ModelCapabilitiesLimits | None = None - """Token limits for prompts, outputs, and context window""" - - supports: ModelCapabilitiesSupports | None = None - """Feature flags indicating what the model supports""" - - @staticmethod - def from_dict(obj: Any) -> 'ModelCapabilities': - assert isinstance(obj, dict) - limits = from_union([ModelCapabilitiesLimits.from_dict, from_none], obj.get("limits")) - supports = from_union([ModelCapabilitiesSupports.from_dict, from_none], obj.get("supports")) - return ModelCapabilities(limits, supports) - - def to_dict(self) -> dict: - result: dict = {} - if self.limits is not None: - result["limits"] = from_union([lambda x: to_class(ModelCapabilitiesLimits, x), from_none], self.limits) - if self.supports is not None: - result["supports"] = from_union([lambda x: to_class(ModelCapabilitiesSupports, x), from_none], self.supports) - return result - # Experimental: this type is part of an experimental API and may change or be removed. class ModelPickerCategory(Enum): """Model capability category for grouping in the model picker""" @@ -42887,6 +45358,175 @@ def to_dict(self) -> dict: result["models"] = from_list(lambda x: to_class(Model, x), self.models) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ProviderModelConfig: + """Model definition prepared from the discovered model. Capability fields the provider did + not report stay omitted rather than being asserted false. + + A BYOK model definition referencing a named provider. + """ + id: str + """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`. + """ + provider: str + """Name of the configured provider that serves this model.""" + + capabilities: ModelCapabilitiesOverride | None = None + """Optional capability overrides (vision, tool_calls, reasoning, etc.).""" + + max_context_window_tokens: float | None = None + """Maximum context window tokens for the model.""" + + max_output_tokens: float | None = None + """Maximum output tokens for the model.""" + + max_prompt_tokens: float | None = None + """Maximum prompt/input tokens for the model.""" + + metadata: dict[str, Any] | None = None + """Provider-published model metadata, preserved verbatim as the public Model.metadata object.""" + + model_id: str | None = None + """Well-known base model id used for behavior/capability/config lookup. Defaults to `id`.""" + + name: str | None = None + """Display name for model pickers. Defaults to the provider-qualified selection id + (`provider/id`). + """ + system_message: ProtocolSystemMessageConfig | None = None + """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. + """ + wire_model: str | None = None + """The model name sent to the provider API for inference. Defaults to `id`.""" + + @staticmethod + def from_dict(obj: Any) -> 'ProviderModelConfig': + assert isinstance(obj, dict) + id = from_str(obj.get("id")) + provider = from_str(obj.get("provider")) + capabilities = from_union([ModelCapabilitiesOverride.from_dict, from_none], obj.get("capabilities")) + max_context_window_tokens = from_union([from_float, from_none], obj.get("maxContextWindowTokens")) + max_output_tokens = from_union([from_float, from_none], obj.get("maxOutputTokens")) + max_prompt_tokens = from_union([from_float, from_none], obj.get("maxPromptTokens")) + metadata = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("metadata")) + model_id = from_union([from_str, from_none], obj.get("modelId")) + name = from_union([from_str, from_none], obj.get("name")) + system_message = from_union([ProtocolSystemMessageConfig.from_dict, from_none], obj.get("systemMessage")) + wire_model = from_union([from_str, from_none], obj.get("wireModel")) + return ProviderModelConfig(id, provider, capabilities, max_context_window_tokens, max_output_tokens, max_prompt_tokens, metadata, model_id, name, system_message, wire_model) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(self.id) + result["provider"] = from_str(self.provider) + if self.capabilities is not None: + result["capabilities"] = from_union([lambda x: to_class(ModelCapabilitiesOverride, x), from_none], self.capabilities) + if self.max_context_window_tokens is not None: + result["maxContextWindowTokens"] = from_union([to_float, from_none], self.max_context_window_tokens) + if self.max_output_tokens is not None: + result["maxOutputTokens"] = from_union([to_float, from_none], self.max_output_tokens) + if self.max_prompt_tokens is not None: + result["maxPromptTokens"] = from_union([to_float, from_none], self.max_prompt_tokens) + if self.metadata is not None: + result["metadata"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.metadata) + if self.model_id is not None: + result["modelId"] = from_union([from_str, from_none], self.model_id) + if self.name is not None: + result["name"] = from_union([from_str, from_none], self.name) + if self.system_message is not None: + result["systemMessage"] = from_union([lambda x: to_class(ProtocolSystemMessageConfig, x), from_none], self.system_message) + if self.wire_model is not None: + result["wireModel"] = from_union([from_str, from_none], self.wire_model) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderConfigurationPlan: + """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. + """ + model: ProviderModelConfig + """Model definition prepared from the discovered model. Capability fields the provider did + not report stay omitted rather than being asserted false. + """ + model_disposition: ModelProviderConfigurationDisposition + """Whether `model` still needs to be registered. When `alreadyConfigured`, `selectionId` is + already registered and the caller can select it without adding anything. + """ + provider: NamedProviderConfig + """Provider connection prepared from the instance's inference metadata. Carries no + credential; supply one if the endpoint requires it. + """ + provider_disposition: ModelProviderConfigurationDisposition + """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. + """ + selection_id: str + """Provider-qualified selection id (`provider/id`) to pass to `switchTo` once the plan is + applied. + """ + warnings: list[ModelProviderWarning] + """Non-fatal warnings carried over from the discovered model, such as capabilities the + provider did not report. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderConfigurationPlan': + assert isinstance(obj, dict) + model = ProviderModelConfig.from_dict(obj.get("model")) + model_disposition = ModelProviderConfigurationDisposition(obj.get("modelDisposition")) + provider = NamedProviderConfig.from_dict(obj.get("provider")) + provider_disposition = ModelProviderConfigurationDisposition(obj.get("providerDisposition")) + selection_id = from_str(obj.get("selectionId")) + warnings = from_list(ModelProviderWarning.from_dict, obj.get("warnings")) + return ModelProviderConfigurationPlan(model, model_disposition, provider, provider_disposition, selection_id, warnings) + + def to_dict(self) -> dict: + result: dict = {} + result["model"] = to_class(ProviderModelConfig, self.model) + result["modelDisposition"] = to_enum(ModelProviderConfigurationDisposition, self.model_disposition) + result["provider"] = to_class(NamedProviderConfig, self.provider) + result["providerDisposition"] = to_enum(ModelProviderConfigurationDisposition, self.provider_disposition) + result["selectionId"] = from_str(self.selection_id) + result["warnings"] = from_list(lambda x: to_class(ModelProviderWarning, x), self.warnings) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelProviderPrepareConfigurationRequest: + """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`. + """ + instance: ModelProviderInstance + """The discovered instance that serves the model.""" + + model: DiscoveredModel + """The discovered model to configure.""" + + @staticmethod + def from_dict(obj: Any) -> 'ModelProviderPrepareConfigurationRequest': + assert isinstance(obj, dict) + instance = ModelProviderInstance.from_dict(obj.get("instance")) + model = DiscoveredModel.from_dict(obj.get("model")) + return ModelProviderPrepareConfigurationRequest(instance, model) + + def to_dict(self) -> dict: + result: dict = {} + result["instance"] = to_class(ModelProviderInstance, self.instance) + result["model"] = to_class(DiscoveredModel, self.model) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ModelSwitchToRequest: @@ -43156,91 +45796,6 @@ def to_dict(self) -> dict: result["title"] = from_union([from_str, from_none], self.title) return result -# Experimental: this type is part of an experimental API and may change or be removed. -@dataclass -class ProviderModelConfig: - """A BYOK model definition referencing a named provider.""" - - id: str - """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`. - """ - provider: str - """Name of the configured provider that serves this model.""" - - capabilities: ModelCapabilitiesOverride | None = None - """Optional capability overrides (vision, tool_calls, reasoning, etc.).""" - - max_context_window_tokens: float | None = None - """Maximum context window tokens for the model.""" - - max_output_tokens: float | None = None - """Maximum output tokens for the model.""" - - max_prompt_tokens: float | None = None - """Maximum prompt/input tokens for the model.""" - - metadata: dict[str, Any] | None = None - """Provider-published model metadata, preserved verbatim as the public Model.metadata object.""" - - model_id: str | None = None - """Well-known base model id used for behavior/capability/config lookup. Defaults to `id`.""" - - name: str | None = None - """Display name for model pickers. Defaults to the provider-qualified selection id - (`provider/id`). - """ - system_message: ProtocolSystemMessageConfig | None = None - """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. - """ - wire_model: str | None = None - """The model name sent to the provider API for inference. Defaults to `id`.""" - - @staticmethod - def from_dict(obj: Any) -> 'ProviderModelConfig': - assert isinstance(obj, dict) - id = from_str(obj.get("id")) - provider = from_str(obj.get("provider")) - capabilities = from_union([ModelCapabilitiesOverride.from_dict, from_none], obj.get("capabilities")) - max_context_window_tokens = from_union([from_float, from_none], obj.get("maxContextWindowTokens")) - max_output_tokens = from_union([from_float, from_none], obj.get("maxOutputTokens")) - max_prompt_tokens = from_union([from_float, from_none], obj.get("maxPromptTokens")) - metadata = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("metadata")) - model_id = from_union([from_str, from_none], obj.get("modelId")) - name = from_union([from_str, from_none], obj.get("name")) - system_message = from_union([ProtocolSystemMessageConfig.from_dict, from_none], obj.get("systemMessage")) - wire_model = from_union([from_str, from_none], obj.get("wireModel")) - return ProviderModelConfig(id, provider, capabilities, max_context_window_tokens, max_output_tokens, max_prompt_tokens, metadata, model_id, name, system_message, wire_model) - - def to_dict(self) -> dict: - result: dict = {} - result["id"] = from_str(self.id) - result["provider"] = from_str(self.provider) - if self.capabilities is not None: - result["capabilities"] = from_union([lambda x: to_class(ModelCapabilitiesOverride, x), from_none], self.capabilities) - if self.max_context_window_tokens is not None: - result["maxContextWindowTokens"] = from_union([to_float, from_none], self.max_context_window_tokens) - if self.max_output_tokens is not None: - result["maxOutputTokens"] = from_union([to_float, from_none], self.max_output_tokens) - if self.max_prompt_tokens is not None: - result["maxPromptTokens"] = from_union([to_float, from_none], self.max_prompt_tokens) - if self.metadata is not None: - result["metadata"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.metadata) - if self.model_id is not None: - result["modelId"] = from_union([from_str, from_none], self.model_id) - if self.name is not None: - result["name"] = from_union([from_str, from_none], self.name) - if self.system_message is not None: - result["systemMessage"] = from_union([lambda x: to_class(ProtocolSystemMessageConfig, x), from_none], self.system_message) - if self.wire_model is not None: - result["wireModel"] = from_union([from_str, from_none], self.wire_model) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ProviderAddRequest: @@ -44392,6 +46947,38 @@ def to_dict(self) -> dict: result["copilotUser"] = from_union([lambda x: to_class(CopilotUserResponse, x), from_none], self.copilot_user) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ShellExecRequest: + """Shell command to run, with optional working directory and timeout in milliseconds. Spawn + failures return an RPC error. + """ + command: str + """Shell command to execute""" + + cwd: str | None = None + """Working directory (defaults to session working directory)""" + + timeout: int | None = None + """Timeout in milliseconds (default: 30000)""" + + @staticmethod + def from_dict(obj: Any) -> 'ShellExecRequest': + assert isinstance(obj, dict) + command = from_str(obj.get("command")) + cwd = from_union([from_str, from_none], obj.get("cwd")) + timeout = from_union([from_int, from_none], obj.get("timeout")) + return ShellExecRequest(command, cwd, timeout) + + def to_dict(self) -> dict: + result: dict = {} + result["command"] = from_str(self.command) + if self.cwd is not None: + result["cwd"] = from_union([from_str, from_none], self.cwd) + if self.timeout is not None: + result["timeout"] = from_union([from_int, from_none], self.timeout) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SlashCommandModelPickerDialog: @@ -45023,6 +47610,7 @@ class RPC: abort_result: AbortResult accepted_enqueue_command_result: AcceptedEnqueueCommandResult account_all_users: AccountAllUsers + account_auth_info: AccountAuthInfo account_get_all_users_result: list[AccountAllUsers] account_get_current_auth_result: AccountGetCurrentAuthResult account_get_quota_request: AccountGetQuotaRequest @@ -45072,8 +47660,10 @@ class RPC: auth_enumerate_query: AuthEnumerateQuery auth_enumerate_value: AuthEnumerateValue auth_identity: AuthIdentity + auth_identity_metadata: AuthIdentityMetadata auth_info: AuthInfo auth_info_type: AuthInfoType + auth_login_account: AuthLoginAccount auth_login_advance_request: AuthLoginAdvanceRequest auth_login_begin_request: AuthLoginBeginRequest auth_login_begun: AuthLoginBegun @@ -45251,6 +47841,7 @@ class RPC: connector_mcp_status: ConnectorMCPStatus connector_reconcile_request: ConnectorReconcileRequest connector_runtime_status: ConnectorRuntimeStatus + connector_session_account: ConnectorSessionAccount connector_status: ConnectorStatus connect_remote_session_params: ConnectRemoteSessionParams connect_request: _ConnectRequest @@ -45269,6 +47860,10 @@ class RPC: copilot_user_response_quota_snapshots_premium_interactions: CopilotUserResponseQuotaSnapshotsPremiumInteractions current_model: CurrentModel current_tool_metadata: CurrentToolMetadata + customization_reload_outcome: CustomizationReloadOutcome + customization_reload_status: CustomizationReloadStatus + customization_reload_subsystem: CustomizationReloadSubsystem + customizations_reload_result: CustomizationsReloadResult debug_collect_logs_collected_entry: DebugCollectLogsCollectedEntry debug_collect_logs_destination: DebugCollectLogsDestination debug_collect_logs_entry: DebugCollectLogsEntry @@ -45301,12 +47896,19 @@ class RPC: discovered_hook: DiscoveredHook discovered_mcp_server: DiscoveredMCPServer discovered_mcp_server_type: DiscoveredMCPServerType + discovered_model: DiscoveredModel + discovered_model_list: DiscoveredModelList enqueue_command_params: EnqueueCommandParams enqueue_command_result: EnqueueCommandResult - entra_token_acquire_request: EntraTokenAcquireRequest - entra_token_acquire_result: EntraTokenAcquireResult - entra_token_interaction: EntraTokenInteraction env_auth_info: EnvAuthInfo + environment_capabilities: EnvironmentCapabilities + environment_kind: EnvironmentKind + environments_delete_request: EnvironmentsDeleteRequest + environments_delete_result: EnvironmentsDeleteResult + environments_get_request: EnvironmentsGetRequest + environments_get_result: EnvironmentsGetResult + environments_list_request: EnvironmentsListRequest + environments_list_result: EnvironmentsListResult event_log_read_request: EventLogReadRequest event_log_release_interest_result: EventLogReleaseInterestResult event_log_tail_result: EventLogTailResult @@ -45349,6 +47951,7 @@ class RPC: folder_trust_check_params: FolderTrustCheckParams folder_trust_check_result: FolderTrustCheckResult gh_cli_auth_info: GhCLIAuthInfo + git_hub_environment: GitHubEnvironment git_hub_telemetry_client_info: GitHubTelemetryClientInfo git_hub_telemetry_event: GitHubTelemetryEvent git_hub_telemetry_notification: GitHubTelemetryNotification @@ -45390,8 +47993,12 @@ class RPC: host_configuration: HostConfiguration host_dispose_request: HostDisposeRequest host_empty_result: HostEmptyResult + host_environment_credentials: HostEnvironmentCredentials host_exited_notification: HostExitedNotification host_exit_reason: HostExitReason + host_git_hub_environment_options: HostGitHubEnvironmentOptions + host_local_server_configuration: HostLocalServerConfiguration + host_local_server_options: HostLocalServerOptions host_publish_session_request: HostPublishSessionRequest host_publish_session_result: HostPublishSessionResult host_ready_request: HostReadyRequest @@ -45445,8 +48052,23 @@ class RPC: log_result: LogResult lsp_initialize_request: LspInitializeRequest managed_mcp_server_config: ManagedMCPServerConfig + managed_setting_meta: ManagedSettingMeta + managed_settings_channel: ManagedSettingsChannel + managed_settings_compose_layer: ManagedSettingsComposeLayer + managed_settings_compose_request: ManagedSettingsComposeRequest + managed_settings_compose_result: ManagedSettingsComposeResult + managed_settings_diagnostic: ManagedSettingsDiagnostic + managed_settings_diagnostic_severity: ManagedSettingsDiagnosticSeverity + managed_settings_layer: ManagedSettingsLayer + managed_settings_meta: ManagedSettingsMeta managed_settings_read_result: ManagedSettingsReadResult managed_settings_resolved_data: ManagedSettingsResolvedData + managed_settings_resolve_request: ManagedSettingsResolveRequest + managed_settings_resolve_result: ManagedSettingsResolveResult + managed_settings_schema_result: ManagedSettingsSchemaResult + managed_settings_validate_request: ManagedSettingsValidateRequest + managed_settings_validate_result: ManagedSettingsValidateResult + managed_settings_values: ManagedSettingsValues marketplace_add_result: MarketplaceAddResult marketplace_browse_result: MarketplaceBrowseResult marketplace_info: MarketplaceInfo @@ -45488,6 +48110,9 @@ class RPC: mcp_config_list: MCPConfigList mcp_config_remove_request: MCPConfigRemoveRequest mcp_config_update_request: MCPConfigUpdateRequest + mcp_configured_server: MCPConfiguredServer + mcp_configured_server_list: MCPConfiguredServerList + mcp_configured_server_state: MCPConfiguredServerState mcp_configure_git_hub_request: MCPConfigureGitHubRequest mcp_configure_git_hub_result: MCPConfigureGitHubResult mcp_diagnostic_details: MCPDiagnosticDetails @@ -45531,6 +48156,7 @@ class RPC: mcp_oauth_authentication_state_changed_request: MCPOauthAuthenticationStateChangedRequest mcp_oauth_cancel_login_request: MCPOauthCancelLoginRequest mcp_oauth_cancel_login_result: MCPOauthCancelLoginResult + mcp_oauth_complete_request: MCPOauthCompleteRequest mcp_oauth_handle_pending_request: MCPOauthHandlePendingRequest mcp_oauth_handle_pending_result: MCPOauthHandlePendingResult mcp_oauth_login_grant_type: MCPGrantType @@ -45582,6 +48208,15 @@ class RPC: mcp_plan_value_category: MCPPlanValueCategory mcp_prepared_install: MCPPreparedInstall mcp_prepare_install_request: MCPPrepareInstallRequest + mcp_prompt: MCPPrompt + mcp_prompt_argument: MCPPromptArgument + mcp_prompt_icon: MCPPromptIcon + mcp_prompt_message: MCPPromptMessage + mcp_prompt_role: MCPPromptRole + mcp_prompts_get_request: MCPPromptsGetRequest + mcp_prompts_get_result: MCPPromptsGetResult + mcp_prompts_list_request: MCPPromptsListRequest + mcp_prompts_list_result: MCPPromptsListResult mcp_register_external_client_request: MCPRegisterExternalClientRequest mcp_reload_config: MCPReloadConfig mcp_reload_with_config_request: MCPReloadWithConfigRequest @@ -45660,6 +48295,7 @@ class RPC: metadata_update_client_metadata_request: MetadataUpdateClientMetadataRequest model: Model model_apply_startup_overlay_request: ModelApplyStartupOverlayRequest + model_artifact_details: ModelArtifactDetails model_billing: ModelBilling model_billing_promo: ModelBillingPromo model_billing_token_prices: ModelBillingTokenPrices @@ -45681,9 +48317,31 @@ class RPC: model_picker_settings_context: ModelPickerSettingsContext model_policy: ModelPolicy model_policy_state: ModelPolicyState + model_provider_adapter_catalog: ModelProviderAdapterCatalog + model_provider_adapter_descriptor: ModelProviderAdapterDescriptor + model_provider_adapter_operation_descriptor: ModelProviderAdapterOperationDescriptor + model_provider_attribution: ModelProviderAttribution + model_provider_automatic_discovery_mode: ModelProviderAutomaticDiscoveryMode + model_provider_automatic_discovery_policy: ModelProviderAutomaticDiscoveryPolicy + model_provider_configuration_disposition: ModelProviderConfigurationDisposition + model_provider_configuration_plan: ModelProviderConfigurationPlan model_provider_descriptor: ModelProviderDescriptor + model_provider_discover_request: ModelProviderDiscoverRequest + model_provider_discover_result: ModelProviderDiscoverResult + model_provider_discovery_network_scope: ModelProviderDiscoveryNetworkScope + model_provider_get_status_request: ModelProviderGetStatusRequest + model_provider_instance: ModelProviderInstance + model_provider_instance_reference: ModelProviderInstanceReference model_provider_kind: ModelProviderKind + model_provider_models_list_request: ModelProviderModelsListRequest + model_provider_operation_outcome: ModelProviderOperationOutcome + model_provider_operation_outcome_code: ModelProviderOperationOutcomeCode + model_provider_prepare_configuration_request: ModelProviderPrepareConfigurationRequest + model_provider_provenance: ModelProviderProvenance + model_provider_provenance_source: ModelProviderProvenanceSource model_provider_ref: ModelProviderRef + model_provider_status: ModelProviderStatus + model_provider_warning: ModelProviderWarning model_set_allowed_models_request: ModelSetAllowedModelsRequest model_set_allowed_models_result: ModelSetAllowedModelsResult model_set_reasoning_effort_request: ModelSetReasoningEffortRequest @@ -45865,6 +48523,7 @@ class RPC: provider_add_result: ProviderAddResult provider_config: ProviderConfig provider_config_azure: ProviderConfigAzure + provider_config_model_provider: ProviderConfigModelProvider provider_config_transport: ProviderTransport provider_config_type: ProviderType provider_config_wire_api: ProviderWireAPI @@ -45880,6 +48539,8 @@ class RPC: provider_sync_result: ProviderSyncResult provider_token_acquire_request: ProviderTokenAcquireRequest provider_token_acquire_result: ProviderTokenAcquireResult + provider_withdraw_request: ProviderWithdrawRequest + provider_withdraw_result: ProviderWithdrawResult push_attachment: PushAttachment push_attachment_blob: PushAttachmentBlob push_attachment_directory: PushAttachmentDirectory @@ -45981,6 +48642,10 @@ class RPC: sandbox_host_capability_name: str sandbox_host_support: SandboxHostSupport sandbox_masked_env_var: SandboxMaskedEnvVar + sandbox_proxy_ca_create_result: SandboxProxyCACreateResult + sandbox_proxy_ca_request: SandboxProxyCARequest + sandbox_proxy_ca_state: SandboxProxyCAState + sandbox_proxy_ca_status: SandboxProxyCAStatus sandbox_session_change: SandboxSessionChange schedule_add_at_request: ScheduleAddAtRequest schedule_add_cron_request: ScheduleAddCronRequest @@ -46034,6 +48699,8 @@ class RPC: session_fs_readdir_with_types_entry_type: DebugCollectLogsEntryKind session_fs_readdir_with_types_request: SessionFSReaddirWithTypesRequest session_fs_readdir_with_types_result: SessionFSReaddirWithTypesResult + session_fs_read_file_bytes_request: SessionFSReadFileBytesRequest + session_fs_read_file_bytes_result: SessionFSReadFileBytesResult session_fs_read_file_request: SessionFSReadFileRequest session_fs_read_file_result: SessionFSReadFileResult session_fs_rename_request: SessionFSRenameRequest @@ -46054,6 +48721,7 @@ class RPC: session_fs_sqlite_transaction_statement: SessionFSSqliteTransactionStatement session_fs_stat_request: SessionFSStatRequest session_fs_stat_result: SessionFSStatResult + session_fs_write_file_bytes_request: SessionFSWriteFileBytesRequest session_fs_write_file_request: SessionFSWriteFileRequest session_git_hub_auth_get_all_auth_available_result: list[SessionAuthStatus] session_git_hub_auth_logout_result: bool @@ -46463,6 +49131,7 @@ class RPC: workspaces_workspace_details_host_type: HostType workspaces_write_autopilot_objective_request: WorkspacesWriteAutopilotObjectiveRequest workspaces_write_autopilot_objective_result: WorkspacesWriteAutopilotObjectiveResult + connector_session_account_result: ConnectorSessionAccount | None = None session_auth_info_result: AuthIdentity | None = None session_context_attribution: SessionContextAttribution | None = None session_context_info: SessionContextInfo | None = None @@ -46477,6 +49146,7 @@ def from_dict(obj: Any) -> 'RPC': abort_result = AbortResult.from_dict(obj.get("AbortResult")) accepted_enqueue_command_result = AcceptedEnqueueCommandResult.from_dict(obj.get("AcceptedEnqueueCommandResult")) account_all_users = AccountAllUsers.from_dict(obj.get("AccountAllUsers")) + account_auth_info = AccountAuthInfo.from_dict(obj.get("AccountAuthInfo")) account_get_all_users_result = from_list(AccountAllUsers.from_dict, obj.get("AccountGetAllUsersResult")) account_get_current_auth_result = AccountGetCurrentAuthResult.from_dict(obj.get("AccountGetCurrentAuthResult")) account_get_quota_request = AccountGetQuotaRequest.from_dict(obj.get("AccountGetQuotaRequest")) @@ -46526,8 +49196,10 @@ def from_dict(obj: Any) -> 'RPC': auth_enumerate_query = AuthEnumerateQuery.from_dict(obj.get("AuthEnumerateQuery")) auth_enumerate_value = AuthEnumerateValue.from_dict(obj.get("AuthEnumerateValue")) auth_identity = AuthIdentity.from_dict(obj.get("AuthIdentity")) + auth_identity_metadata = AuthIdentityMetadata.from_dict(obj.get("AuthIdentityMetadata")) auth_info = _load_AuthInfo(obj.get("AuthInfo")) auth_info_type = AuthInfoType(obj.get("AuthInfoType")) + auth_login_account = AuthLoginAccount.from_dict(obj.get("AuthLoginAccount")) auth_login_advance_request = AuthLoginAdvanceRequest.from_dict(obj.get("AuthLoginAdvanceRequest")) auth_login_begin_request = AuthLoginBeginRequest.from_dict(obj.get("AuthLoginBeginRequest")) auth_login_begun = AuthLoginBegun.from_dict(obj.get("AuthLoginBegun")) @@ -46705,6 +49377,7 @@ def from_dict(obj: Any) -> 'RPC': connector_mcp_status = ConnectorMCPStatus(obj.get("ConnectorMcpStatus")) connector_reconcile_request = ConnectorReconcileRequest.from_dict(obj.get("ConnectorReconcileRequest")) connector_runtime_status = ConnectorRuntimeStatus.from_dict(obj.get("ConnectorRuntimeStatus")) + connector_session_account = ConnectorSessionAccount.from_dict(obj.get("ConnectorSessionAccount")) connector_status = ConnectorStatus.from_dict(obj.get("ConnectorStatus")) connect_remote_session_params = ConnectRemoteSessionParams.from_dict(obj.get("ConnectRemoteSessionParams")) connect_request = _ConnectRequest.from_dict(obj.get("ConnectRequest")) @@ -46723,6 +49396,10 @@ def from_dict(obj: Any) -> 'RPC': copilot_user_response_quota_snapshots_premium_interactions = CopilotUserResponseQuotaSnapshotsPremiumInteractions.from_dict(obj.get("CopilotUserResponseQuotaSnapshotsPremiumInteractions")) current_model = CurrentModel.from_dict(obj.get("CurrentModel")) current_tool_metadata = CurrentToolMetadata.from_dict(obj.get("CurrentToolMetadata")) + customization_reload_outcome = CustomizationReloadOutcome.from_dict(obj.get("CustomizationReloadOutcome")) + customization_reload_status = CustomizationReloadStatus(obj.get("CustomizationReloadStatus")) + customization_reload_subsystem = CustomizationReloadSubsystem(obj.get("CustomizationReloadSubsystem")) + customizations_reload_result = CustomizationsReloadResult.from_dict(obj.get("CustomizationsReloadResult")) debug_collect_logs_collected_entry = DebugCollectLogsCollectedEntry.from_dict(obj.get("DebugCollectLogsCollectedEntry")) debug_collect_logs_destination = DebugCollectLogsDestination.from_dict(obj.get("DebugCollectLogsDestination")) debug_collect_logs_entry = DebugCollectLogsEntry.from_dict(obj.get("DebugCollectLogsEntry")) @@ -46755,12 +49432,19 @@ def from_dict(obj: Any) -> 'RPC': discovered_hook = DiscoveredHook.from_dict(obj.get("DiscoveredHook")) discovered_mcp_server = DiscoveredMCPServer.from_dict(obj.get("DiscoveredMcpServer")) discovered_mcp_server_type = DiscoveredMCPServerType(obj.get("DiscoveredMcpServerType")) + discovered_model = DiscoveredModel.from_dict(obj.get("DiscoveredModel")) + discovered_model_list = DiscoveredModelList.from_dict(obj.get("DiscoveredModelList")) enqueue_command_params = EnqueueCommandParams.from_dict(obj.get("EnqueueCommandParams")) enqueue_command_result = _load_EnqueueCommandResult(obj.get("EnqueueCommandResult")) - entra_token_acquire_request = EntraTokenAcquireRequest.from_dict(obj.get("EntraTokenAcquireRequest")) - entra_token_acquire_result = EntraTokenAcquireResult.from_dict(obj.get("EntraTokenAcquireResult")) - entra_token_interaction = EntraTokenInteraction(obj.get("EntraTokenInteraction")) env_auth_info = EnvAuthInfo.from_dict(obj.get("EnvAuthInfo")) + environment_capabilities = EnvironmentCapabilities.from_dict(obj.get("EnvironmentCapabilities")) + environment_kind = EnvironmentKind(obj.get("EnvironmentKind")) + environments_delete_request = EnvironmentsDeleteRequest.from_dict(obj.get("EnvironmentsDeleteRequest")) + environments_delete_result = EnvironmentsDeleteResult.from_dict(obj.get("EnvironmentsDeleteResult")) + environments_get_request = EnvironmentsGetRequest.from_dict(obj.get("EnvironmentsGetRequest")) + environments_get_result = EnvironmentsGetResult.from_dict(obj.get("EnvironmentsGetResult")) + environments_list_request = EnvironmentsListRequest.from_dict(obj.get("EnvironmentsListRequest")) + environments_list_result = EnvironmentsListResult.from_dict(obj.get("EnvironmentsListResult")) event_log_read_request = EventLogReadRequest.from_dict(obj.get("EventLogReadRequest")) event_log_release_interest_result = EventLogReleaseInterestResult.from_dict(obj.get("EventLogReleaseInterestResult")) event_log_tail_result = EventLogTailResult.from_dict(obj.get("EventLogTailResult")) @@ -46803,6 +49487,7 @@ def from_dict(obj: Any) -> 'RPC': folder_trust_check_params = FolderTrustCheckParams.from_dict(obj.get("FolderTrustCheckParams")) folder_trust_check_result = FolderTrustCheckResult.from_dict(obj.get("FolderTrustCheckResult")) gh_cli_auth_info = GhCLIAuthInfo.from_dict(obj.get("GhCliAuthInfo")) + git_hub_environment = GitHubEnvironment.from_dict(obj.get("GitHubEnvironment")) git_hub_telemetry_client_info = GitHubTelemetryClientInfo.from_dict(obj.get("GitHubTelemetryClientInfo")) git_hub_telemetry_event = GitHubTelemetryEvent.from_dict(obj.get("GitHubTelemetryEvent")) git_hub_telemetry_notification = GitHubTelemetryNotification.from_dict(obj.get("GitHubTelemetryNotification")) @@ -46844,8 +49529,12 @@ def from_dict(obj: Any) -> 'RPC': host_configuration = HostConfiguration.from_dict(obj.get("HostConfiguration")) host_dispose_request = HostDisposeRequest.from_dict(obj.get("HostDisposeRequest")) host_empty_result = HostEmptyResult.from_dict(obj.get("HostEmptyResult")) + host_environment_credentials = HostEnvironmentCredentials.from_dict(obj.get("HostEnvironmentCredentials")) host_exited_notification = HostExitedNotification.from_dict(obj.get("HostExitedNotification")) host_exit_reason = HostExitReason(obj.get("HostExitReason")) + host_git_hub_environment_options = HostGitHubEnvironmentOptions.from_dict(obj.get("HostGitHubEnvironmentOptions")) + host_local_server_configuration = HostLocalServerConfiguration.from_dict(obj.get("HostLocalServerConfiguration")) + host_local_server_options = HostLocalServerOptions.from_dict(obj.get("HostLocalServerOptions")) host_publish_session_request = HostPublishSessionRequest.from_dict(obj.get("HostPublishSessionRequest")) host_publish_session_result = HostPublishSessionResult.from_dict(obj.get("HostPublishSessionResult")) host_ready_request = HostReadyRequest.from_dict(obj.get("HostReadyRequest")) @@ -46899,8 +49588,23 @@ def from_dict(obj: Any) -> 'RPC': log_result = LogResult.from_dict(obj.get("LogResult")) lsp_initialize_request = LspInitializeRequest.from_dict(obj.get("LspInitializeRequest")) managed_mcp_server_config = ManagedMCPServerConfig.from_dict(obj.get("ManagedMcpServerConfig")) + managed_setting_meta = ManagedSettingMeta.from_dict(obj.get("ManagedSettingMeta")) + managed_settings_channel = ManagedSettingsChannel(obj.get("ManagedSettingsChannel")) + managed_settings_compose_layer = ManagedSettingsComposeLayer.from_dict(obj.get("ManagedSettingsComposeLayer")) + managed_settings_compose_request = ManagedSettingsComposeRequest.from_dict(obj.get("ManagedSettingsComposeRequest")) + managed_settings_compose_result = ManagedSettingsComposeResult.from_dict(obj.get("ManagedSettingsComposeResult")) + managed_settings_diagnostic = ManagedSettingsDiagnostic.from_dict(obj.get("ManagedSettingsDiagnostic")) + managed_settings_diagnostic_severity = ManagedSettingsDiagnosticSeverity(obj.get("ManagedSettingsDiagnosticSeverity")) + managed_settings_layer = ManagedSettingsLayer.from_dict(obj.get("ManagedSettingsLayer")) + managed_settings_meta = ManagedSettingsMeta.from_dict(obj.get("ManagedSettingsMeta")) managed_settings_read_result = ManagedSettingsReadResult.from_dict(obj.get("ManagedSettingsReadResult")) managed_settings_resolved_data = ManagedSettingsResolvedData.from_dict(obj.get("ManagedSettingsResolvedData")) + managed_settings_resolve_request = ManagedSettingsResolveRequest.from_dict(obj.get("ManagedSettingsResolveRequest")) + managed_settings_resolve_result = ManagedSettingsResolveResult.from_dict(obj.get("ManagedSettingsResolveResult")) + managed_settings_schema_result = ManagedSettingsSchemaResult.from_dict(obj.get("ManagedSettingsSchemaResult")) + managed_settings_validate_request = ManagedSettingsValidateRequest.from_dict(obj.get("ManagedSettingsValidateRequest")) + managed_settings_validate_result = ManagedSettingsValidateResult.from_dict(obj.get("ManagedSettingsValidateResult")) + managed_settings_values = ManagedSettingsValues.from_dict(obj.get("ManagedSettingsValues")) marketplace_add_result = MarketplaceAddResult.from_dict(obj.get("MarketplaceAddResult")) marketplace_browse_result = MarketplaceBrowseResult.from_dict(obj.get("MarketplaceBrowseResult")) marketplace_info = MarketplaceInfo.from_dict(obj.get("MarketplaceInfo")) @@ -46942,6 +49646,9 @@ def from_dict(obj: Any) -> 'RPC': mcp_config_list = MCPConfigList.from_dict(obj.get("McpConfigList")) mcp_config_remove_request = MCPConfigRemoveRequest.from_dict(obj.get("McpConfigRemoveRequest")) mcp_config_update_request = MCPConfigUpdateRequest.from_dict(obj.get("McpConfigUpdateRequest")) + mcp_configured_server = MCPConfiguredServer.from_dict(obj.get("McpConfiguredServer")) + mcp_configured_server_list = MCPConfiguredServerList.from_dict(obj.get("McpConfiguredServerList")) + mcp_configured_server_state = MCPConfiguredServerState.from_dict(obj.get("McpConfiguredServerState")) mcp_configure_git_hub_request = MCPConfigureGitHubRequest.from_dict(obj.get("McpConfigureGitHubRequest")) mcp_configure_git_hub_result = MCPConfigureGitHubResult.from_dict(obj.get("McpConfigureGitHubResult")) mcp_diagnostic_details = MCPDiagnosticDetails.from_dict(obj.get("McpDiagnosticDetails")) @@ -46985,6 +49692,7 @@ def from_dict(obj: Any) -> 'RPC': mcp_oauth_authentication_state_changed_request = MCPOauthAuthenticationStateChangedRequest.from_dict(obj.get("McpOauthAuthenticationStateChangedRequest")) mcp_oauth_cancel_login_request = MCPOauthCancelLoginRequest.from_dict(obj.get("McpOauthCancelLoginRequest")) mcp_oauth_cancel_login_result = MCPOauthCancelLoginResult.from_dict(obj.get("McpOauthCancelLoginResult")) + mcp_oauth_complete_request = MCPOauthCompleteRequest.from_dict(obj.get("McpOauthCompleteRequest")) mcp_oauth_handle_pending_request = MCPOauthHandlePendingRequest.from_dict(obj.get("McpOauthHandlePendingRequest")) mcp_oauth_handle_pending_result = MCPOauthHandlePendingResult.from_dict(obj.get("McpOauthHandlePendingResult")) mcp_oauth_login_grant_type = MCPGrantType(obj.get("McpOauthLoginGrantType")) @@ -47036,6 +49744,15 @@ def from_dict(obj: Any) -> 'RPC': mcp_plan_value_category = MCPPlanValueCategory(obj.get("McpPlanValueCategory")) mcp_prepared_install = MCPPreparedInstall.from_dict(obj.get("McpPreparedInstall")) mcp_prepare_install_request = MCPPrepareInstallRequest.from_dict(obj.get("McpPrepareInstallRequest")) + mcp_prompt = MCPPrompt.from_dict(obj.get("McpPrompt")) + mcp_prompt_argument = MCPPromptArgument.from_dict(obj.get("McpPromptArgument")) + mcp_prompt_icon = MCPPromptIcon.from_dict(obj.get("McpPromptIcon")) + mcp_prompt_message = MCPPromptMessage.from_dict(obj.get("McpPromptMessage")) + mcp_prompt_role = MCPPromptRole(obj.get("McpPromptRole")) + mcp_prompts_get_request = MCPPromptsGetRequest.from_dict(obj.get("McpPromptsGetRequest")) + mcp_prompts_get_result = MCPPromptsGetResult.from_dict(obj.get("McpPromptsGetResult")) + mcp_prompts_list_request = MCPPromptsListRequest.from_dict(obj.get("McpPromptsListRequest")) + mcp_prompts_list_result = MCPPromptsListResult.from_dict(obj.get("McpPromptsListResult")) mcp_register_external_client_request = MCPRegisterExternalClientRequest.from_dict(obj.get("McpRegisterExternalClientRequest")) mcp_reload_config = MCPReloadConfig.from_dict(obj.get("McpReloadConfig")) mcp_reload_with_config_request = MCPReloadWithConfigRequest.from_dict(obj.get("McpReloadWithConfigRequest")) @@ -47114,6 +49831,7 @@ def from_dict(obj: Any) -> 'RPC': metadata_update_client_metadata_request = MetadataUpdateClientMetadataRequest.from_dict(obj.get("MetadataUpdateClientMetadataRequest")) model = Model.from_dict(obj.get("Model")) model_apply_startup_overlay_request = ModelApplyStartupOverlayRequest.from_dict(obj.get("ModelApplyStartupOverlayRequest")) + model_artifact_details = ModelArtifactDetails.from_dict(obj.get("ModelArtifactDetails")) model_billing = ModelBilling.from_dict(obj.get("ModelBilling")) model_billing_promo = ModelBillingPromo.from_dict(obj.get("ModelBillingPromo")) model_billing_token_prices = ModelBillingTokenPrices.from_dict(obj.get("ModelBillingTokenPrices")) @@ -47135,9 +49853,31 @@ def from_dict(obj: Any) -> 'RPC': model_picker_settings_context = ModelPickerSettingsContext.from_dict(obj.get("ModelPickerSettingsContext")) model_policy = ModelPolicy.from_dict(obj.get("ModelPolicy")) model_policy_state = ModelPolicyState(obj.get("ModelPolicyState")) + model_provider_adapter_catalog = ModelProviderAdapterCatalog.from_dict(obj.get("ModelProviderAdapterCatalog")) + model_provider_adapter_descriptor = ModelProviderAdapterDescriptor.from_dict(obj.get("ModelProviderAdapterDescriptor")) + model_provider_adapter_operation_descriptor = ModelProviderAdapterOperationDescriptor.from_dict(obj.get("ModelProviderAdapterOperationDescriptor")) + model_provider_attribution = ModelProviderAttribution.from_dict(obj.get("ModelProviderAttribution")) + model_provider_automatic_discovery_mode = ModelProviderAutomaticDiscoveryMode(obj.get("ModelProviderAutomaticDiscoveryMode")) + model_provider_automatic_discovery_policy = ModelProviderAutomaticDiscoveryPolicy.from_dict(obj.get("ModelProviderAutomaticDiscoveryPolicy")) + model_provider_configuration_disposition = ModelProviderConfigurationDisposition(obj.get("ModelProviderConfigurationDisposition")) + model_provider_configuration_plan = ModelProviderConfigurationPlan.from_dict(obj.get("ModelProviderConfigurationPlan")) model_provider_descriptor = ModelProviderDescriptor.from_dict(obj.get("ModelProviderDescriptor")) + model_provider_discover_request = ModelProviderDiscoverRequest.from_dict(obj.get("ModelProviderDiscoverRequest")) + model_provider_discover_result = ModelProviderDiscoverResult.from_dict(obj.get("ModelProviderDiscoverResult")) + model_provider_discovery_network_scope = ModelProviderDiscoveryNetworkScope(obj.get("ModelProviderDiscoveryNetworkScope")) + model_provider_get_status_request = ModelProviderGetStatusRequest.from_dict(obj.get("ModelProviderGetStatusRequest")) + model_provider_instance = ModelProviderInstance.from_dict(obj.get("ModelProviderInstance")) + model_provider_instance_reference = ModelProviderInstanceReference.from_dict(obj.get("ModelProviderInstanceReference")) model_provider_kind = ModelProviderKind(obj.get("ModelProviderKind")) + model_provider_models_list_request = ModelProviderModelsListRequest.from_dict(obj.get("ModelProviderModelsListRequest")) + model_provider_operation_outcome = ModelProviderOperationOutcome.from_dict(obj.get("ModelProviderOperationOutcome")) + model_provider_operation_outcome_code = ModelProviderOperationOutcomeCode(obj.get("ModelProviderOperationOutcomeCode")) + model_provider_prepare_configuration_request = ModelProviderPrepareConfigurationRequest.from_dict(obj.get("ModelProviderPrepareConfigurationRequest")) + model_provider_provenance = ModelProviderProvenance.from_dict(obj.get("ModelProviderProvenance")) + model_provider_provenance_source = ModelProviderProvenanceSource(obj.get("ModelProviderProvenanceSource")) model_provider_ref = ModelProviderRef.from_dict(obj.get("ModelProviderRef")) + model_provider_status = ModelProviderStatus.from_dict(obj.get("ModelProviderStatus")) + model_provider_warning = ModelProviderWarning.from_dict(obj.get("ModelProviderWarning")) model_set_allowed_models_request = ModelSetAllowedModelsRequest.from_dict(obj.get("ModelSetAllowedModelsRequest")) model_set_allowed_models_result = ModelSetAllowedModelsResult.from_dict(obj.get("ModelSetAllowedModelsResult")) model_set_reasoning_effort_request = ModelSetReasoningEffortRequest.from_dict(obj.get("ModelSetReasoningEffortRequest")) @@ -47319,6 +50059,7 @@ def from_dict(obj: Any) -> 'RPC': provider_add_result = ProviderAddResult.from_dict(obj.get("ProviderAddResult")) provider_config = ProviderConfig.from_dict(obj.get("ProviderConfig")) provider_config_azure = ProviderConfigAzure.from_dict(obj.get("ProviderConfigAzure")) + provider_config_model_provider = ProviderConfigModelProvider(obj.get("ProviderConfigModelProvider")) provider_config_transport = ProviderTransport(obj.get("ProviderConfigTransport")) provider_config_type = ProviderType(obj.get("ProviderConfigType")) provider_config_wire_api = ProviderWireAPI(obj.get("ProviderConfigWireApi")) @@ -47334,6 +50075,8 @@ def from_dict(obj: Any) -> 'RPC': provider_sync_result = ProviderSyncResult.from_dict(obj.get("ProviderSyncResult")) provider_token_acquire_request = ProviderTokenAcquireRequest.from_dict(obj.get("ProviderTokenAcquireRequest")) provider_token_acquire_result = ProviderTokenAcquireResult.from_dict(obj.get("ProviderTokenAcquireResult")) + provider_withdraw_request = ProviderWithdrawRequest.from_dict(obj.get("ProviderWithdrawRequest")) + provider_withdraw_result = ProviderWithdrawResult.from_dict(obj.get("ProviderWithdrawResult")) push_attachment = _load_PushAttachment(obj.get("PushAttachment")) push_attachment_blob = PushAttachmentBlob.from_dict(obj.get("PushAttachmentBlob")) push_attachment_directory = PushAttachmentDirectory.from_dict(obj.get("PushAttachmentDirectory")) @@ -47435,6 +50178,10 @@ def from_dict(obj: Any) -> 'RPC': sandbox_host_capability_name = from_str(obj.get("SandboxHostCapabilityName")) sandbox_host_support = SandboxHostSupport.from_dict(obj.get("SandboxHostSupport")) sandbox_masked_env_var = SandboxMaskedEnvVar.from_dict(obj.get("SandboxMaskedEnvVar")) + sandbox_proxy_ca_create_result = SandboxProxyCACreateResult.from_dict(obj.get("SandboxProxyCaCreateResult")) + sandbox_proxy_ca_request = SandboxProxyCARequest.from_dict(obj.get("SandboxProxyCaRequest")) + sandbox_proxy_ca_state = SandboxProxyCAState(obj.get("SandboxProxyCaState")) + sandbox_proxy_ca_status = SandboxProxyCAStatus.from_dict(obj.get("SandboxProxyCaStatus")) sandbox_session_change = SandboxSessionChange(obj.get("SandboxSessionChange")) schedule_add_at_request = ScheduleAddAtRequest.from_dict(obj.get("ScheduleAddAtRequest")) schedule_add_cron_request = ScheduleAddCronRequest.from_dict(obj.get("ScheduleAddCronRequest")) @@ -47488,6 +50235,8 @@ def from_dict(obj: Any) -> 'RPC': session_fs_readdir_with_types_entry_type = DebugCollectLogsEntryKind(obj.get("SessionFsReaddirWithTypesEntryType")) session_fs_readdir_with_types_request = SessionFSReaddirWithTypesRequest.from_dict(obj.get("SessionFsReaddirWithTypesRequest")) session_fs_readdir_with_types_result = SessionFSReaddirWithTypesResult.from_dict(obj.get("SessionFsReaddirWithTypesResult")) + session_fs_read_file_bytes_request = SessionFSReadFileBytesRequest.from_dict(obj.get("SessionFsReadFileBytesRequest")) + session_fs_read_file_bytes_result = SessionFSReadFileBytesResult.from_dict(obj.get("SessionFsReadFileBytesResult")) session_fs_read_file_request = SessionFSReadFileRequest.from_dict(obj.get("SessionFsReadFileRequest")) session_fs_read_file_result = SessionFSReadFileResult.from_dict(obj.get("SessionFsReadFileResult")) session_fs_rename_request = SessionFSRenameRequest.from_dict(obj.get("SessionFsRenameRequest")) @@ -47508,6 +50257,7 @@ def from_dict(obj: Any) -> 'RPC': session_fs_sqlite_transaction_statement = SessionFSSqliteTransactionStatement.from_dict(obj.get("SessionFsSqliteTransactionStatement")) session_fs_stat_request = SessionFSStatRequest.from_dict(obj.get("SessionFsStatRequest")) session_fs_stat_result = SessionFSStatResult.from_dict(obj.get("SessionFsStatResult")) + session_fs_write_file_bytes_request = SessionFSWriteFileBytesRequest.from_dict(obj.get("SessionFsWriteFileBytesRequest")) session_fs_write_file_request = SessionFSWriteFileRequest.from_dict(obj.get("SessionFsWriteFileRequest")) session_git_hub_auth_get_all_auth_available_result = from_list(SessionAuthStatus.from_dict, obj.get("SessionGitHubAuthGetAllAuthAvailableResult")) session_git_hub_auth_logout_result = from_bool(obj.get("SessionGitHubAuthLogoutResult")) @@ -47917,13 +50667,14 @@ def from_dict(obj: Any) -> 'RPC': workspaces_workspace_details_host_type = HostType(obj.get("WorkspacesWorkspaceDetailsHostType")) workspaces_write_autopilot_objective_request = WorkspacesWriteAutopilotObjectiveRequest.from_dict(obj.get("WorkspacesWriteAutopilotObjectiveRequest")) workspaces_write_autopilot_objective_result = WorkspacesWriteAutopilotObjectiveResult.from_dict(obj.get("WorkspacesWriteAutopilotObjectiveResult")) + connector_session_account_result = from_union([ConnectorSessionAccount.from_dict, from_none], obj.get("ConnectorSessionAccountResult")) session_auth_info_result = from_union([AuthIdentity.from_dict, from_none], obj.get("SessionAuthInfoResult")) session_context_attribution = from_union([SessionContextAttribution.from_dict, from_none], obj.get("SessionContextAttribution")) session_context_info = from_union([SessionContextInfo.from_dict, from_none], obj.get("SessionContextInfo")) subagent_settings = from_union([SubagentSettings.from_dict, from_none], obj.get("SubagentSettings")) task_progress = from_union([TaskProgress.from_dict, from_none], obj.get("TaskProgress")) workspace_summary = from_union([WorkspaceSummary.from_dict, from_none], obj.get("WorkspaceSummary")) - return RPC(abort_request, abort_result, accepted_enqueue_command_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_kind, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, accounts_enumerate_request, accounts_get_request, accounts_set_request, account_status, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_enumerate_query, auth_enumerate_value, auth_identity, auth_info, auth_info_type, auth_login_advance_request, auth_login_begin_request, auth_login_begun, auth_login_cancel_request, auth_login_result_dto, auth_login_result_status, auth_login_step, auth_read_query, auth_read_value, auth_status_dto, auth_validation_error, auth_validation_errors, auth_write, auth_write_result, autopilot_objective_credit_limit, autopilot_objective_get_state_result, autopilot_objective_state, autopilot_objective_status, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_agent_plugin_candidate, catalog_agent_plugin_candidate_kind, catalog_agent_plugin_candidate_provenance, catalog_agent_plugin_compatibility_tag, catalog_agent_plugin_media_type, catalog_ai_skill_candidate, catalog_ai_skill_candidate_kind, catalog_ai_skill_candidate_provenance, catalog_ai_skill_installability, catalog_ai_skill_media_type, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_kind, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_plugin_repository_source, catalog_policy_rejected_error, catalog_resource_identity, catalog_resource_version, catalog_search_page, catalog_search_pagination, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_search_total_count_relation, catalog_selection_cancelled, catalog_selection_decision, catalog_selection_declined, catalog_selection_foreign, catalog_selection_invalid, catalog_selection_replayed, catalog_selection_request, catalog_selection_result, catalog_selection_selected, catalog_selection_stale, catalog_selection_timed_out, catalog_selection_wrong_kind, catalog_trust_eligibility, catalog_trust_provenance, catalog_trust_snapshot, catalog_trust_snapshot_absent, catalog_trust_snapshot_absent_status, catalog_trust_snapshot_current, catalog_trust_snapshot_current_status, catalog_trust_snapshot_downgraded, catalog_trust_snapshot_downgraded_status, catalog_trust_snapshot_malformed, catalog_trust_snapshot_malformed_status, catalog_trust_snapshot_revoked, catalog_trust_snapshot_revoked_status, catalog_trust_snapshot_schema_version, catalog_trust_snapshot_stale, catalog_trust_snapshot_stale_status, catalog_trust_snapshot_unsupported, catalog_trust_snapshot_unsupported_status, catalog_trust_source, catalog_trust_tier, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, client_metadata, client_task_cancel_reason, client_task_cancel_request, client_task_cancel_result, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connector_account_request, connector_authorization_requirement, connector_authorization_scope, connector_availability, connector_capabilities, connector_catalog_entry, connector_catalog_result, connector_catalog_status, connector_connect_request, connector_connect_result, connector_continue_request, connector_disconnect_result, connector_mcp_status, connector_reconcile_request, connector_runtime_status, connector_status, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, diagnostic_cursor_status, diagnostic_entry, diagnostic_log_level, diagnostics_configuration, diagnostics_configure_request, diagnostic_severity, diagnostic_source, diagnostic_sources_configuration, diagnostics_read_request, diagnostics_read_result, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, entra_token_acquire_request, entra_token_acquire_result, entra_token_interaction, env_auth_info, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, host_configuration, host_dispose_request, host_empty_result, host_exited_notification, host_exit_reason, host_publish_session_request, host_publish_session_result, host_ready_request, host_register_session_request, host_session_create_callback, host_session_create_request, host_session_create_result, host_session_released_notification, host_session_release_request, host_start_request, host_start_result, installation_catalogue_identity, installation_confirmation_request, installation_confirmation_response, installation_decision, installation_review, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, interrupt_main_turn_request, interrupt_main_turn_result, json_schema_response_format, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, login_provider_kind, log_request, log_result, lsp_initialize_request, managed_mcp_server_config, managed_settings_read_result, managed_settings_resolved_data, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apply_install_request, mcp_apply_uninstall_request, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_diagnostic_details, mcp_diagnostic_direction, mcp_diagnostic_kind, mcp_diagnostic_source_configuration, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, mcp_filtered_server, mcp_headers_handle_pending_headers_refresh_request, mcp_headers_handle_pending_headers_refresh_request_request, mcp_headers_handle_pending_headers_refresh_request_result, mcp_host_state, mcp_installation_failure_reason, mcp_installation_input, mcp_installation_management_outcome, mcp_installation_management_result, mcp_installation_operation_request, mcp_installation_operation_status, mcp_installation_outcome, mcp_installation_remote_configuration, mcp_installation_result, mcp_installation_review, mcp_installation_secret, mcp_installation_secret_storage, mcp_installations_request, mcp_installation_state, mcp_installation_summary, mcp_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, mcp_oauth_cancel_login_request, mcp_oauth_cancel_login_result, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_prepare_login_request, mcp_oauth_prepare_login_result, mcp_oauth_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_owned_oauth_login_status, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_uninstall_request, mcp_plan_value_category, mcp_prepared_install, mcp_prepare_install_request, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_server_ownership, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_source_file, mcp_source_plugin, mcp_source_ref, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_uninstall_plan, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, metadata_update_client_metadata_request, model, model_apply_startup_overlay_request, model_billing, model_billing_promo, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_provider_descriptor, model_provider_kind, model_provider_ref, model_set_allowed_models_request, model_set_allowed_models_result, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_auto_tier_request, model_switch_auto_tier_result, model_switch_auto_tier_status, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_workflow, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_workflow, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_approve_read_only_for_session, permission_decision_cancelled, permission_decision_context, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_outcome, permission_decision_reject, permission_decision_request, permission_decision_surface, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_mode_source, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_response_capability, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_mode_request, permissions_get_mode_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_workflow, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_install_staging_mode, plugin_list, plugin_list_result, plugins_builtin_set_request, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, protocol_append_mode, protocol_customize_mode, protocol_external_tool_defer, protocol_external_tool_definition, protocol_marker_section_override, protocol_replace_mode, protocol_section_override, protocol_static_section_action, protocol_static_section_override, protocol_system_message_append_config, protocol_system_message_config, protocol_system_message_customize_config, protocol_system_message_replace_config, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_transport, provider_config_type, provider_config_wire_api, provider_descriptor, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_sync_request, provider_sync_result, provider_token_acquire_request, provider_token_acquire_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_append_steering_request, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_result, queue_withdraw_message_request, queue_withdraw_message_result, register_event_interest_params, register_event_interest_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, response_format, sandbox_config, sandbox_config_auth, sandbox_config_source, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_credentials_config, sandbox_disable_for_session_request, sandbox_disable_for_session_result, sandbox_enforcement_status, sandbox_grant_path_for_request_request, sandbox_grant_path_for_request_result, sandbox_host_capability, sandbox_host_capability_name, sandbox_host_support, sandbox_masked_env_var, sandbox_session_change, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_mcp_oauth_cancel_login_request, session_mcp_oauth_cancel_login_result, session_mcp_oauth_prepare_login_request, session_mcp_oauth_prepare_login_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_plugins_disable_request, session_plugins_enable_request, session_plugins_install_request, session_plugins_marketplaces_refresh_request, session_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_client_metadata_entry, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_client_metadata_request, sessions_get_client_metadata_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_prune_old_request, sessions_read_persisted_events_request, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_visibility_status, session_workflow_pause_at_checkpoint_result, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_apply_install_request, skill_apply_uninstall_request, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_installation_failure_reason, skill_installation_file_review, skill_installation_location, skill_installation_management_outcome, skill_installation_management_result, skill_installation_operation_request, skill_installation_operation_status, skill_installation_outcome, skill_installation_ownership_state, skill_installation_result, skill_installation_review, skill_installation_scope, skill_installation_session_state, skill_installation_source, skill_installations_request, skill_installation_summary, skill_install_plan, skill_list, skill_plan_install_request, skill_plan_uninstall_request, skill_provider_descriptor, skill_provider_list_request, skill_provider_list_result, skill_provider_read_request, skill_provider_read_result, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_request, skills_disable_request, skills_discover_request, skills_enable_request, skill_set_enabled_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, skill_uninstall_plan, slash_command_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, system_message_block, task_agent_info, task_agent_progress, task_client_active_status, task_client_execution_mode, task_client_info, task_client_owner, task_client_owner_kind, task_client_owner_presence, task_client_progress, task_client_status, task_client_type, task_client_update, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_kind, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_register_request, tasks_register_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_update_request, tasks_update_result, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, unsupported_enqueue_command_result, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_agent_metric, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workflow_abort_request, workflow_ack_result, workflow_agent_options, workflow_agent_request, workflow_agent_result, workflow_agent_summary, workflow_cancel_request, workflow_current_phase, workflow_declared_limits, workflow_durable_operation, workflow_execute_request, workflow_execute_result, workflow_get_run_progress_request, workflow_get_run_request, workflow_journal_get_request, workflow_journal_get_result, workflow_journal_put_request, workflow_list_runs_request, workflow_list_runs_result, workflow_log_line, workflow_log_line_kind, workflow_log_request, workflow_pause_checkpoint_action, workflow_pause_checkpoint_request, workflow_pause_checkpoint_result, workflow_pause_info, workflow_pause_request, workflow_phase_observation, workflow_phase_status, workflow_progress_line, workflow_progress_page, workflow_resume_request, workflow_resume_result, workflow_run_consumed, workflow_run_detail, workflow_run_failure, workflow_run_failure_kind, workflow_run_limits, workflow_run_options, workflow_run_request, workflow_run_result, workflow_run_status, workflow_run_summary, workflow_run_terminal, workflow_tool_resume_request, workflow_tool_run_options, workflow_tool_run_request, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_directory_request, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_remove_path_request, workspaces_rename_path_request, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspaces_stat_file_request, workspaces_stat_file_result, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) + return RPC(abort_request, abort_result, accepted_enqueue_command_result, account_all_users, account_auth_info, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_kind, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, accounts_enumerate_request, accounts_get_request, accounts_set_request, account_status, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_enumerate_query, auth_enumerate_value, auth_identity, auth_identity_metadata, auth_info, auth_info_type, auth_login_account, auth_login_advance_request, auth_login_begin_request, auth_login_begun, auth_login_cancel_request, auth_login_result_dto, auth_login_result_status, auth_login_step, auth_read_query, auth_read_value, auth_status_dto, auth_validation_error, auth_validation_errors, auth_write, auth_write_result, autopilot_objective_credit_limit, autopilot_objective_get_state_result, autopilot_objective_state, autopilot_objective_status, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_agent_plugin_candidate, catalog_agent_plugin_candidate_kind, catalog_agent_plugin_candidate_provenance, catalog_agent_plugin_compatibility_tag, catalog_agent_plugin_media_type, catalog_ai_skill_candidate, catalog_ai_skill_candidate_kind, catalog_ai_skill_candidate_provenance, catalog_ai_skill_installability, catalog_ai_skill_media_type, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_kind, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_plugin_repository_source, catalog_policy_rejected_error, catalog_resource_identity, catalog_resource_version, catalog_search_page, catalog_search_pagination, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_search_total_count_relation, catalog_selection_cancelled, catalog_selection_decision, catalog_selection_declined, catalog_selection_foreign, catalog_selection_invalid, catalog_selection_replayed, catalog_selection_request, catalog_selection_result, catalog_selection_selected, catalog_selection_stale, catalog_selection_timed_out, catalog_selection_wrong_kind, catalog_trust_eligibility, catalog_trust_provenance, catalog_trust_snapshot, catalog_trust_snapshot_absent, catalog_trust_snapshot_absent_status, catalog_trust_snapshot_current, catalog_trust_snapshot_current_status, catalog_trust_snapshot_downgraded, catalog_trust_snapshot_downgraded_status, catalog_trust_snapshot_malformed, catalog_trust_snapshot_malformed_status, catalog_trust_snapshot_revoked, catalog_trust_snapshot_revoked_status, catalog_trust_snapshot_schema_version, catalog_trust_snapshot_stale, catalog_trust_snapshot_stale_status, catalog_trust_snapshot_unsupported, catalog_trust_snapshot_unsupported_status, catalog_trust_source, catalog_trust_tier, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, client_metadata, client_task_cancel_reason, client_task_cancel_request, client_task_cancel_result, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connector_account_request, connector_authorization_requirement, connector_authorization_scope, connector_availability, connector_capabilities, connector_catalog_entry, connector_catalog_result, connector_catalog_status, connector_connect_request, connector_connect_result, connector_continue_request, connector_disconnect_result, connector_mcp_status, connector_reconcile_request, connector_runtime_status, connector_session_account, connector_status, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, customization_reload_outcome, customization_reload_status, customization_reload_subsystem, customizations_reload_result, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, diagnostic_cursor_status, diagnostic_entry, diagnostic_log_level, diagnostics_configuration, diagnostics_configure_request, diagnostic_severity, diagnostic_source, diagnostic_sources_configuration, diagnostics_read_request, diagnostics_read_result, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, discovered_mcp_server, discovered_mcp_server_type, discovered_model, discovered_model_list, enqueue_command_params, enqueue_command_result, env_auth_info, environment_capabilities, environment_kind, environments_delete_request, environments_delete_result, environments_get_request, environments_get_result, environments_list_request, environments_list_result, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_environment, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, host_configuration, host_dispose_request, host_empty_result, host_environment_credentials, host_exited_notification, host_exit_reason, host_git_hub_environment_options, host_local_server_configuration, host_local_server_options, host_publish_session_request, host_publish_session_result, host_ready_request, host_register_session_request, host_session_create_callback, host_session_create_request, host_session_create_result, host_session_released_notification, host_session_release_request, host_start_request, host_start_result, installation_catalogue_identity, installation_confirmation_request, installation_confirmation_response, installation_decision, installation_review, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, interrupt_main_turn_request, interrupt_main_turn_result, json_schema_response_format, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, login_provider_kind, log_request, log_result, lsp_initialize_request, managed_mcp_server_config, managed_setting_meta, managed_settings_channel, managed_settings_compose_layer, managed_settings_compose_request, managed_settings_compose_result, managed_settings_diagnostic, managed_settings_diagnostic_severity, managed_settings_layer, managed_settings_meta, managed_settings_read_result, managed_settings_resolved_data, managed_settings_resolve_request, managed_settings_resolve_result, managed_settings_schema_result, managed_settings_validate_request, managed_settings_validate_result, managed_settings_values, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apply_install_request, mcp_apply_uninstall_request, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configured_server, mcp_configured_server_list, mcp_configured_server_state, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_diagnostic_details, mcp_diagnostic_direction, mcp_diagnostic_kind, mcp_diagnostic_source_configuration, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, mcp_filtered_server, mcp_headers_handle_pending_headers_refresh_request, mcp_headers_handle_pending_headers_refresh_request_request, mcp_headers_handle_pending_headers_refresh_request_result, mcp_host_state, mcp_installation_failure_reason, mcp_installation_input, mcp_installation_management_outcome, mcp_installation_management_result, mcp_installation_operation_request, mcp_installation_operation_status, mcp_installation_outcome, mcp_installation_remote_configuration, mcp_installation_result, mcp_installation_review, mcp_installation_secret, mcp_installation_secret_storage, mcp_installations_request, mcp_installation_state, mcp_installation_summary, mcp_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, mcp_oauth_cancel_login_request, mcp_oauth_cancel_login_result, mcp_oauth_complete_request, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_prepare_login_request, mcp_oauth_prepare_login_result, mcp_oauth_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_owned_oauth_login_status, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_uninstall_request, mcp_plan_value_category, mcp_prepared_install, mcp_prepare_install_request, mcp_prompt, mcp_prompt_argument, mcp_prompt_icon, mcp_prompt_message, mcp_prompt_role, mcp_prompts_get_request, mcp_prompts_get_result, mcp_prompts_list_request, mcp_prompts_list_result, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_server_ownership, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_source_file, mcp_source_plugin, mcp_source_ref, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_uninstall_plan, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, metadata_update_client_metadata_request, model, model_apply_startup_overlay_request, model_artifact_details, model_billing, model_billing_promo, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_provider_adapter_catalog, model_provider_adapter_descriptor, model_provider_adapter_operation_descriptor, model_provider_attribution, model_provider_automatic_discovery_mode, model_provider_automatic_discovery_policy, model_provider_configuration_disposition, model_provider_configuration_plan, model_provider_descriptor, model_provider_discover_request, model_provider_discover_result, model_provider_discovery_network_scope, model_provider_get_status_request, model_provider_instance, model_provider_instance_reference, model_provider_kind, model_provider_models_list_request, model_provider_operation_outcome, model_provider_operation_outcome_code, model_provider_prepare_configuration_request, model_provider_provenance, model_provider_provenance_source, model_provider_ref, model_provider_status, model_provider_warning, model_set_allowed_models_request, model_set_allowed_models_result, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_auto_tier_request, model_switch_auto_tier_result, model_switch_auto_tier_status, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_workflow, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_workflow, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_approve_read_only_for_session, permission_decision_cancelled, permission_decision_context, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_outcome, permission_decision_reject, permission_decision_request, permission_decision_surface, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_mode_source, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_response_capability, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_mode_request, permissions_get_mode_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_workflow, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_install_staging_mode, plugin_list, plugin_list_result, plugins_builtin_set_request, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, protocol_append_mode, protocol_customize_mode, protocol_external_tool_defer, protocol_external_tool_definition, protocol_marker_section_override, protocol_replace_mode, protocol_section_override, protocol_static_section_action, protocol_static_section_override, protocol_system_message_append_config, protocol_system_message_config, protocol_system_message_customize_config, protocol_system_message_replace_config, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_model_provider, provider_config_transport, provider_config_type, provider_config_wire_api, provider_descriptor, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_sync_request, provider_sync_result, provider_token_acquire_request, provider_token_acquire_result, provider_withdraw_request, provider_withdraw_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_append_steering_request, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_result, queue_withdraw_message_request, queue_withdraw_message_result, register_event_interest_params, register_event_interest_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, response_format, sandbox_config, sandbox_config_auth, sandbox_config_source, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_credentials_config, sandbox_disable_for_session_request, sandbox_disable_for_session_result, sandbox_enforcement_status, sandbox_grant_path_for_request_request, sandbox_grant_path_for_request_result, sandbox_host_capability, sandbox_host_capability_name, sandbox_host_support, sandbox_masked_env_var, sandbox_proxy_ca_create_result, sandbox_proxy_ca_request, sandbox_proxy_ca_state, sandbox_proxy_ca_status, sandbox_session_change, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_bytes_request, session_fs_read_file_bytes_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_bytes_request, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_mcp_oauth_cancel_login_request, session_mcp_oauth_cancel_login_result, session_mcp_oauth_prepare_login_request, session_mcp_oauth_prepare_login_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_plugins_disable_request, session_plugins_enable_request, session_plugins_install_request, session_plugins_marketplaces_refresh_request, session_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_client_metadata_entry, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_client_metadata_request, sessions_get_client_metadata_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_prune_old_request, sessions_read_persisted_events_request, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_visibility_status, session_workflow_pause_at_checkpoint_result, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_apply_install_request, skill_apply_uninstall_request, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_installation_failure_reason, skill_installation_file_review, skill_installation_location, skill_installation_management_outcome, skill_installation_management_result, skill_installation_operation_request, skill_installation_operation_status, skill_installation_outcome, skill_installation_ownership_state, skill_installation_result, skill_installation_review, skill_installation_scope, skill_installation_session_state, skill_installation_source, skill_installations_request, skill_installation_summary, skill_install_plan, skill_list, skill_plan_install_request, skill_plan_uninstall_request, skill_provider_descriptor, skill_provider_list_request, skill_provider_list_result, skill_provider_read_request, skill_provider_read_result, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_request, skills_disable_request, skills_discover_request, skills_enable_request, skill_set_enabled_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, skill_uninstall_plan, slash_command_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, system_message_block, task_agent_info, task_agent_progress, task_client_active_status, task_client_execution_mode, task_client_info, task_client_owner, task_client_owner_kind, task_client_owner_presence, task_client_progress, task_client_status, task_client_type, task_client_update, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_kind, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_register_request, tasks_register_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_update_request, tasks_update_result, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, unsupported_enqueue_command_result, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_agent_metric, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workflow_abort_request, workflow_ack_result, workflow_agent_options, workflow_agent_request, workflow_agent_result, workflow_agent_summary, workflow_cancel_request, workflow_current_phase, workflow_declared_limits, workflow_durable_operation, workflow_execute_request, workflow_execute_result, workflow_get_run_progress_request, workflow_get_run_request, workflow_journal_get_request, workflow_journal_get_result, workflow_journal_put_request, workflow_list_runs_request, workflow_list_runs_result, workflow_log_line, workflow_log_line_kind, workflow_log_request, workflow_pause_checkpoint_action, workflow_pause_checkpoint_request, workflow_pause_checkpoint_result, workflow_pause_info, workflow_pause_request, workflow_phase_observation, workflow_phase_status, workflow_progress_line, workflow_progress_page, workflow_resume_request, workflow_resume_result, workflow_run_consumed, workflow_run_detail, workflow_run_failure, workflow_run_failure_kind, workflow_run_limits, workflow_run_options, workflow_run_request, workflow_run_result, workflow_run_status, workflow_run_summary, workflow_run_terminal, workflow_tool_resume_request, workflow_tool_run_options, workflow_tool_run_request, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_directory_request, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_remove_path_request, workspaces_rename_path_request, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspaces_stat_file_request, workspaces_stat_file_result, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, connector_session_account_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) def to_dict(self) -> dict: result: dict = {} @@ -47931,6 +50682,7 @@ def to_dict(self) -> dict: result["AbortResult"] = to_class(AbortResult, self.abort_result) result["AcceptedEnqueueCommandResult"] = to_class(AcceptedEnqueueCommandResult, self.accepted_enqueue_command_result) result["AccountAllUsers"] = to_class(AccountAllUsers, self.account_all_users) + result["AccountAuthInfo"] = to_class(AccountAuthInfo, self.account_auth_info) result["AccountGetAllUsersResult"] = from_list(lambda x: to_class(AccountAllUsers, x), self.account_get_all_users_result) result["AccountGetCurrentAuthResult"] = to_class(AccountGetCurrentAuthResult, self.account_get_current_auth_result) result["AccountGetQuotaRequest"] = to_class(AccountGetQuotaRequest, self.account_get_quota_request) @@ -47980,8 +50732,10 @@ def to_dict(self) -> dict: result["AuthEnumerateQuery"] = to_class(AuthEnumerateQuery, self.auth_enumerate_query) result["AuthEnumerateValue"] = to_class(AuthEnumerateValue, self.auth_enumerate_value) result["AuthIdentity"] = to_class(AuthIdentity, self.auth_identity) + result["AuthIdentityMetadata"] = to_class(AuthIdentityMetadata, self.auth_identity_metadata) result["AuthInfo"] = (self.auth_info).to_dict() result["AuthInfoType"] = to_enum(AuthInfoType, self.auth_info_type) + result["AuthLoginAccount"] = to_class(AuthLoginAccount, self.auth_login_account) result["AuthLoginAdvanceRequest"] = to_class(AuthLoginAdvanceRequest, self.auth_login_advance_request) result["AuthLoginBeginRequest"] = to_class(AuthLoginBeginRequest, self.auth_login_begin_request) result["AuthLoginBegun"] = to_class(AuthLoginBegun, self.auth_login_begun) @@ -48159,6 +50913,7 @@ def to_dict(self) -> dict: result["ConnectorMcpStatus"] = to_enum(ConnectorMCPStatus, self.connector_mcp_status) result["ConnectorReconcileRequest"] = to_class(ConnectorReconcileRequest, self.connector_reconcile_request) result["ConnectorRuntimeStatus"] = to_class(ConnectorRuntimeStatus, self.connector_runtime_status) + result["ConnectorSessionAccount"] = to_class(ConnectorSessionAccount, self.connector_session_account) result["ConnectorStatus"] = to_class(ConnectorStatus, self.connector_status) result["ConnectRemoteSessionParams"] = to_class(ConnectRemoteSessionParams, self.connect_remote_session_params) result["ConnectRequest"] = to_class(_ConnectRequest, self.connect_request) @@ -48177,6 +50932,10 @@ def to_dict(self) -> dict: result["CopilotUserResponseQuotaSnapshotsPremiumInteractions"] = to_class(CopilotUserResponseQuotaSnapshotsPremiumInteractions, self.copilot_user_response_quota_snapshots_premium_interactions) result["CurrentModel"] = to_class(CurrentModel, self.current_model) result["CurrentToolMetadata"] = to_class(CurrentToolMetadata, self.current_tool_metadata) + result["CustomizationReloadOutcome"] = to_class(CustomizationReloadOutcome, self.customization_reload_outcome) + result["CustomizationReloadStatus"] = to_enum(CustomizationReloadStatus, self.customization_reload_status) + result["CustomizationReloadSubsystem"] = to_enum(CustomizationReloadSubsystem, self.customization_reload_subsystem) + result["CustomizationsReloadResult"] = to_class(CustomizationsReloadResult, self.customizations_reload_result) result["DebugCollectLogsCollectedEntry"] = to_class(DebugCollectLogsCollectedEntry, self.debug_collect_logs_collected_entry) result["DebugCollectLogsDestination"] = to_class(DebugCollectLogsDestination, self.debug_collect_logs_destination) result["DebugCollectLogsEntry"] = to_class(DebugCollectLogsEntry, self.debug_collect_logs_entry) @@ -48209,12 +50968,19 @@ def to_dict(self) -> dict: result["DiscoveredHook"] = to_class(DiscoveredHook, self.discovered_hook) result["DiscoveredMcpServer"] = to_class(DiscoveredMCPServer, self.discovered_mcp_server) result["DiscoveredMcpServerType"] = to_enum(DiscoveredMCPServerType, self.discovered_mcp_server_type) + result["DiscoveredModel"] = to_class(DiscoveredModel, self.discovered_model) + result["DiscoveredModelList"] = to_class(DiscoveredModelList, self.discovered_model_list) result["EnqueueCommandParams"] = to_class(EnqueueCommandParams, self.enqueue_command_params) result["EnqueueCommandResult"] = (self.enqueue_command_result).to_dict() - result["EntraTokenAcquireRequest"] = to_class(EntraTokenAcquireRequest, self.entra_token_acquire_request) - result["EntraTokenAcquireResult"] = to_class(EntraTokenAcquireResult, self.entra_token_acquire_result) - result["EntraTokenInteraction"] = to_enum(EntraTokenInteraction, self.entra_token_interaction) result["EnvAuthInfo"] = to_class(EnvAuthInfo, self.env_auth_info) + result["EnvironmentCapabilities"] = to_class(EnvironmentCapabilities, self.environment_capabilities) + result["EnvironmentKind"] = to_enum(EnvironmentKind, self.environment_kind) + result["EnvironmentsDeleteRequest"] = to_class(EnvironmentsDeleteRequest, self.environments_delete_request) + result["EnvironmentsDeleteResult"] = to_class(EnvironmentsDeleteResult, self.environments_delete_result) + result["EnvironmentsGetRequest"] = to_class(EnvironmentsGetRequest, self.environments_get_request) + result["EnvironmentsGetResult"] = to_class(EnvironmentsGetResult, self.environments_get_result) + result["EnvironmentsListRequest"] = to_class(EnvironmentsListRequest, self.environments_list_request) + result["EnvironmentsListResult"] = to_class(EnvironmentsListResult, self.environments_list_result) result["EventLogReadRequest"] = to_class(EventLogReadRequest, self.event_log_read_request) result["EventLogReleaseInterestResult"] = to_class(EventLogReleaseInterestResult, self.event_log_release_interest_result) result["EventLogTailResult"] = to_class(EventLogTailResult, self.event_log_tail_result) @@ -48257,6 +51023,7 @@ def to_dict(self) -> dict: result["FolderTrustCheckParams"] = to_class(FolderTrustCheckParams, self.folder_trust_check_params) result["FolderTrustCheckResult"] = to_class(FolderTrustCheckResult, self.folder_trust_check_result) result["GhCliAuthInfo"] = to_class(GhCLIAuthInfo, self.gh_cli_auth_info) + result["GitHubEnvironment"] = to_class(GitHubEnvironment, self.git_hub_environment) result["GitHubTelemetryClientInfo"] = to_class(GitHubTelemetryClientInfo, self.git_hub_telemetry_client_info) result["GitHubTelemetryEvent"] = to_class(GitHubTelemetryEvent, self.git_hub_telemetry_event) result["GitHubTelemetryNotification"] = to_class(GitHubTelemetryNotification, self.git_hub_telemetry_notification) @@ -48298,8 +51065,12 @@ def to_dict(self) -> dict: result["HostConfiguration"] = to_class(HostConfiguration, self.host_configuration) result["HostDisposeRequest"] = to_class(HostDisposeRequest, self.host_dispose_request) result["HostEmptyResult"] = to_class(HostEmptyResult, self.host_empty_result) + result["HostEnvironmentCredentials"] = to_class(HostEnvironmentCredentials, self.host_environment_credentials) result["HostExitedNotification"] = to_class(HostExitedNotification, self.host_exited_notification) result["HostExitReason"] = to_enum(HostExitReason, self.host_exit_reason) + result["HostGitHubEnvironmentOptions"] = to_class(HostGitHubEnvironmentOptions, self.host_git_hub_environment_options) + result["HostLocalServerConfiguration"] = to_class(HostLocalServerConfiguration, self.host_local_server_configuration) + result["HostLocalServerOptions"] = to_class(HostLocalServerOptions, self.host_local_server_options) result["HostPublishSessionRequest"] = to_class(HostPublishSessionRequest, self.host_publish_session_request) result["HostPublishSessionResult"] = to_class(HostPublishSessionResult, self.host_publish_session_result) result["HostReadyRequest"] = to_class(HostReadyRequest, self.host_ready_request) @@ -48353,8 +51124,23 @@ def to_dict(self) -> dict: result["LogResult"] = to_class(LogResult, self.log_result) result["LspInitializeRequest"] = to_class(LspInitializeRequest, self.lsp_initialize_request) result["ManagedMcpServerConfig"] = to_class(ManagedMCPServerConfig, self.managed_mcp_server_config) + result["ManagedSettingMeta"] = to_class(ManagedSettingMeta, self.managed_setting_meta) + result["ManagedSettingsChannel"] = to_enum(ManagedSettingsChannel, self.managed_settings_channel) + result["ManagedSettingsComposeLayer"] = to_class(ManagedSettingsComposeLayer, self.managed_settings_compose_layer) + result["ManagedSettingsComposeRequest"] = to_class(ManagedSettingsComposeRequest, self.managed_settings_compose_request) + result["ManagedSettingsComposeResult"] = to_class(ManagedSettingsComposeResult, self.managed_settings_compose_result) + result["ManagedSettingsDiagnostic"] = to_class(ManagedSettingsDiagnostic, self.managed_settings_diagnostic) + result["ManagedSettingsDiagnosticSeverity"] = to_enum(ManagedSettingsDiagnosticSeverity, self.managed_settings_diagnostic_severity) + result["ManagedSettingsLayer"] = to_class(ManagedSettingsLayer, self.managed_settings_layer) + result["ManagedSettingsMeta"] = to_class(ManagedSettingsMeta, self.managed_settings_meta) result["ManagedSettingsReadResult"] = to_class(ManagedSettingsReadResult, self.managed_settings_read_result) result["ManagedSettingsResolvedData"] = to_class(ManagedSettingsResolvedData, self.managed_settings_resolved_data) + result["ManagedSettingsResolveRequest"] = to_class(ManagedSettingsResolveRequest, self.managed_settings_resolve_request) + result["ManagedSettingsResolveResult"] = to_class(ManagedSettingsResolveResult, self.managed_settings_resolve_result) + result["ManagedSettingsSchemaResult"] = to_class(ManagedSettingsSchemaResult, self.managed_settings_schema_result) + result["ManagedSettingsValidateRequest"] = to_class(ManagedSettingsValidateRequest, self.managed_settings_validate_request) + result["ManagedSettingsValidateResult"] = to_class(ManagedSettingsValidateResult, self.managed_settings_validate_result) + result["ManagedSettingsValues"] = to_class(ManagedSettingsValues, self.managed_settings_values) result["MarketplaceAddResult"] = to_class(MarketplaceAddResult, self.marketplace_add_result) result["MarketplaceBrowseResult"] = to_class(MarketplaceBrowseResult, self.marketplace_browse_result) result["MarketplaceInfo"] = to_class(MarketplaceInfo, self.marketplace_info) @@ -48396,6 +51182,9 @@ def to_dict(self) -> dict: result["McpConfigList"] = to_class(MCPConfigList, self.mcp_config_list) result["McpConfigRemoveRequest"] = to_class(MCPConfigRemoveRequest, self.mcp_config_remove_request) result["McpConfigUpdateRequest"] = to_class(MCPConfigUpdateRequest, self.mcp_config_update_request) + result["McpConfiguredServer"] = to_class(MCPConfiguredServer, self.mcp_configured_server) + result["McpConfiguredServerList"] = to_class(MCPConfiguredServerList, self.mcp_configured_server_list) + result["McpConfiguredServerState"] = to_class(MCPConfiguredServerState, self.mcp_configured_server_state) result["McpConfigureGitHubRequest"] = to_class(MCPConfigureGitHubRequest, self.mcp_configure_git_hub_request) result["McpConfigureGitHubResult"] = to_class(MCPConfigureGitHubResult, self.mcp_configure_git_hub_result) result["McpDiagnosticDetails"] = to_class(MCPDiagnosticDetails, self.mcp_diagnostic_details) @@ -48439,6 +51228,7 @@ def to_dict(self) -> dict: result["McpOauthAuthenticationStateChangedRequest"] = to_class(MCPOauthAuthenticationStateChangedRequest, self.mcp_oauth_authentication_state_changed_request) result["McpOauthCancelLoginRequest"] = to_class(MCPOauthCancelLoginRequest, self.mcp_oauth_cancel_login_request) result["McpOauthCancelLoginResult"] = to_class(MCPOauthCancelLoginResult, self.mcp_oauth_cancel_login_result) + result["McpOauthCompleteRequest"] = to_class(MCPOauthCompleteRequest, self.mcp_oauth_complete_request) result["McpOauthHandlePendingRequest"] = to_class(MCPOauthHandlePendingRequest, self.mcp_oauth_handle_pending_request) result["McpOauthHandlePendingResult"] = to_class(MCPOauthHandlePendingResult, self.mcp_oauth_handle_pending_result) result["McpOauthLoginGrantType"] = to_enum(MCPGrantType, self.mcp_oauth_login_grant_type) @@ -48490,6 +51280,15 @@ def to_dict(self) -> dict: result["McpPlanValueCategory"] = to_enum(MCPPlanValueCategory, self.mcp_plan_value_category) result["McpPreparedInstall"] = to_class(MCPPreparedInstall, self.mcp_prepared_install) result["McpPrepareInstallRequest"] = to_class(MCPPrepareInstallRequest, self.mcp_prepare_install_request) + result["McpPrompt"] = to_class(MCPPrompt, self.mcp_prompt) + result["McpPromptArgument"] = to_class(MCPPromptArgument, self.mcp_prompt_argument) + result["McpPromptIcon"] = to_class(MCPPromptIcon, self.mcp_prompt_icon) + result["McpPromptMessage"] = to_class(MCPPromptMessage, self.mcp_prompt_message) + result["McpPromptRole"] = to_enum(MCPPromptRole, self.mcp_prompt_role) + result["McpPromptsGetRequest"] = to_class(MCPPromptsGetRequest, self.mcp_prompts_get_request) + result["McpPromptsGetResult"] = to_class(MCPPromptsGetResult, self.mcp_prompts_get_result) + result["McpPromptsListRequest"] = to_class(MCPPromptsListRequest, self.mcp_prompts_list_request) + result["McpPromptsListResult"] = to_class(MCPPromptsListResult, self.mcp_prompts_list_result) result["McpRegisterExternalClientRequest"] = to_class(MCPRegisterExternalClientRequest, self.mcp_register_external_client_request) result["McpReloadConfig"] = to_class(MCPReloadConfig, self.mcp_reload_config) result["McpReloadWithConfigRequest"] = to_class(MCPReloadWithConfigRequest, self.mcp_reload_with_config_request) @@ -48568,6 +51367,7 @@ def to_dict(self) -> dict: result["MetadataUpdateClientMetadataRequest"] = to_class(MetadataUpdateClientMetadataRequest, self.metadata_update_client_metadata_request) result["Model"] = to_class(Model, self.model) result["ModelApplyStartupOverlayRequest"] = to_class(ModelApplyStartupOverlayRequest, self.model_apply_startup_overlay_request) + result["ModelArtifactDetails"] = to_class(ModelArtifactDetails, self.model_artifact_details) result["ModelBilling"] = to_class(ModelBilling, self.model_billing) result["ModelBillingPromo"] = to_class(ModelBillingPromo, self.model_billing_promo) result["ModelBillingTokenPrices"] = to_class(ModelBillingTokenPrices, self.model_billing_token_prices) @@ -48589,9 +51389,31 @@ def to_dict(self) -> dict: result["ModelPickerSettingsContext"] = to_class(ModelPickerSettingsContext, self.model_picker_settings_context) result["ModelPolicy"] = to_class(ModelPolicy, self.model_policy) result["ModelPolicyState"] = to_enum(ModelPolicyState, self.model_policy_state) + result["ModelProviderAdapterCatalog"] = to_class(ModelProviderAdapterCatalog, self.model_provider_adapter_catalog) + result["ModelProviderAdapterDescriptor"] = to_class(ModelProviderAdapterDescriptor, self.model_provider_adapter_descriptor) + result["ModelProviderAdapterOperationDescriptor"] = to_class(ModelProviderAdapterOperationDescriptor, self.model_provider_adapter_operation_descriptor) + result["ModelProviderAttribution"] = to_class(ModelProviderAttribution, self.model_provider_attribution) + result["ModelProviderAutomaticDiscoveryMode"] = to_enum(ModelProviderAutomaticDiscoveryMode, self.model_provider_automatic_discovery_mode) + result["ModelProviderAutomaticDiscoveryPolicy"] = to_class(ModelProviderAutomaticDiscoveryPolicy, self.model_provider_automatic_discovery_policy) + result["ModelProviderConfigurationDisposition"] = to_enum(ModelProviderConfigurationDisposition, self.model_provider_configuration_disposition) + result["ModelProviderConfigurationPlan"] = to_class(ModelProviderConfigurationPlan, self.model_provider_configuration_plan) result["ModelProviderDescriptor"] = to_class(ModelProviderDescriptor, self.model_provider_descriptor) + result["ModelProviderDiscoverRequest"] = to_class(ModelProviderDiscoverRequest, self.model_provider_discover_request) + result["ModelProviderDiscoverResult"] = to_class(ModelProviderDiscoverResult, self.model_provider_discover_result) + result["ModelProviderDiscoveryNetworkScope"] = to_enum(ModelProviderDiscoveryNetworkScope, self.model_provider_discovery_network_scope) + result["ModelProviderGetStatusRequest"] = to_class(ModelProviderGetStatusRequest, self.model_provider_get_status_request) + result["ModelProviderInstance"] = to_class(ModelProviderInstance, self.model_provider_instance) + result["ModelProviderInstanceReference"] = to_class(ModelProviderInstanceReference, self.model_provider_instance_reference) result["ModelProviderKind"] = to_enum(ModelProviderKind, self.model_provider_kind) + result["ModelProviderModelsListRequest"] = to_class(ModelProviderModelsListRequest, self.model_provider_models_list_request) + result["ModelProviderOperationOutcome"] = to_class(ModelProviderOperationOutcome, self.model_provider_operation_outcome) + result["ModelProviderOperationOutcomeCode"] = to_enum(ModelProviderOperationOutcomeCode, self.model_provider_operation_outcome_code) + result["ModelProviderPrepareConfigurationRequest"] = to_class(ModelProviderPrepareConfigurationRequest, self.model_provider_prepare_configuration_request) + result["ModelProviderProvenance"] = to_class(ModelProviderProvenance, self.model_provider_provenance) + result["ModelProviderProvenanceSource"] = to_enum(ModelProviderProvenanceSource, self.model_provider_provenance_source) result["ModelProviderRef"] = to_class(ModelProviderRef, self.model_provider_ref) + result["ModelProviderStatus"] = to_class(ModelProviderStatus, self.model_provider_status) + result["ModelProviderWarning"] = to_class(ModelProviderWarning, self.model_provider_warning) result["ModelSetAllowedModelsRequest"] = to_class(ModelSetAllowedModelsRequest, self.model_set_allowed_models_request) result["ModelSetAllowedModelsResult"] = to_class(ModelSetAllowedModelsResult, self.model_set_allowed_models_result) result["ModelSetReasoningEffortRequest"] = to_class(ModelSetReasoningEffortRequest, self.model_set_reasoning_effort_request) @@ -48773,6 +51595,7 @@ def to_dict(self) -> dict: result["ProviderAddResult"] = to_class(ProviderAddResult, self.provider_add_result) result["ProviderConfig"] = to_class(ProviderConfig, self.provider_config) result["ProviderConfigAzure"] = to_class(ProviderConfigAzure, self.provider_config_azure) + result["ProviderConfigModelProvider"] = to_enum(ProviderConfigModelProvider, self.provider_config_model_provider) result["ProviderConfigTransport"] = to_enum(ProviderTransport, self.provider_config_transport) result["ProviderConfigType"] = to_enum(ProviderType, self.provider_config_type) result["ProviderConfigWireApi"] = to_enum(ProviderWireAPI, self.provider_config_wire_api) @@ -48788,6 +51611,8 @@ def to_dict(self) -> dict: result["ProviderSyncResult"] = to_class(ProviderSyncResult, self.provider_sync_result) result["ProviderTokenAcquireRequest"] = to_class(ProviderTokenAcquireRequest, self.provider_token_acquire_request) result["ProviderTokenAcquireResult"] = to_class(ProviderTokenAcquireResult, self.provider_token_acquire_result) + result["ProviderWithdrawRequest"] = to_class(ProviderWithdrawRequest, self.provider_withdraw_request) + result["ProviderWithdrawResult"] = to_class(ProviderWithdrawResult, self.provider_withdraw_result) result["PushAttachment"] = (self.push_attachment).to_dict() result["PushAttachmentBlob"] = to_class(PushAttachmentBlob, self.push_attachment_blob) result["PushAttachmentDirectory"] = to_class(PushAttachmentDirectory, self.push_attachment_directory) @@ -48889,6 +51714,10 @@ def to_dict(self) -> dict: result["SandboxHostCapabilityName"] = from_str(self.sandbox_host_capability_name) result["SandboxHostSupport"] = to_class(SandboxHostSupport, self.sandbox_host_support) result["SandboxMaskedEnvVar"] = to_class(SandboxMaskedEnvVar, self.sandbox_masked_env_var) + result["SandboxProxyCaCreateResult"] = to_class(SandboxProxyCACreateResult, self.sandbox_proxy_ca_create_result) + result["SandboxProxyCaRequest"] = to_class(SandboxProxyCARequest, self.sandbox_proxy_ca_request) + result["SandboxProxyCaState"] = to_enum(SandboxProxyCAState, self.sandbox_proxy_ca_state) + result["SandboxProxyCaStatus"] = to_class(SandboxProxyCAStatus, self.sandbox_proxy_ca_status) result["SandboxSessionChange"] = to_enum(SandboxSessionChange, self.sandbox_session_change) result["ScheduleAddAtRequest"] = to_class(ScheduleAddAtRequest, self.schedule_add_at_request) result["ScheduleAddCronRequest"] = to_class(ScheduleAddCronRequest, self.schedule_add_cron_request) @@ -48942,6 +51771,8 @@ def to_dict(self) -> dict: result["SessionFsReaddirWithTypesEntryType"] = to_enum(DebugCollectLogsEntryKind, self.session_fs_readdir_with_types_entry_type) result["SessionFsReaddirWithTypesRequest"] = to_class(SessionFSReaddirWithTypesRequest, self.session_fs_readdir_with_types_request) result["SessionFsReaddirWithTypesResult"] = to_class(SessionFSReaddirWithTypesResult, self.session_fs_readdir_with_types_result) + result["SessionFsReadFileBytesRequest"] = to_class(SessionFSReadFileBytesRequest, self.session_fs_read_file_bytes_request) + result["SessionFsReadFileBytesResult"] = to_class(SessionFSReadFileBytesResult, self.session_fs_read_file_bytes_result) result["SessionFsReadFileRequest"] = to_class(SessionFSReadFileRequest, self.session_fs_read_file_request) result["SessionFsReadFileResult"] = to_class(SessionFSReadFileResult, self.session_fs_read_file_result) result["SessionFsRenameRequest"] = to_class(SessionFSRenameRequest, self.session_fs_rename_request) @@ -48962,6 +51793,7 @@ def to_dict(self) -> dict: result["SessionFsSqliteTransactionStatement"] = to_class(SessionFSSqliteTransactionStatement, self.session_fs_sqlite_transaction_statement) result["SessionFsStatRequest"] = to_class(SessionFSStatRequest, self.session_fs_stat_request) result["SessionFsStatResult"] = to_class(SessionFSStatResult, self.session_fs_stat_result) + result["SessionFsWriteFileBytesRequest"] = to_class(SessionFSWriteFileBytesRequest, self.session_fs_write_file_bytes_request) result["SessionFsWriteFileRequest"] = to_class(SessionFSWriteFileRequest, self.session_fs_write_file_request) result["SessionGitHubAuthGetAllAuthAvailableResult"] = from_list(lambda x: to_class(SessionAuthStatus, x), self.session_git_hub_auth_get_all_auth_available_result) result["SessionGitHubAuthLogoutResult"] = from_bool(self.session_git_hub_auth_logout_result) @@ -49371,6 +52203,7 @@ def to_dict(self) -> dict: result["WorkspacesWorkspaceDetailsHostType"] = to_enum(HostType, self.workspaces_workspace_details_host_type) result["WorkspacesWriteAutopilotObjectiveRequest"] = to_class(WorkspacesWriteAutopilotObjectiveRequest, self.workspaces_write_autopilot_objective_request) result["WorkspacesWriteAutopilotObjectiveResult"] = to_class(WorkspacesWriteAutopilotObjectiveResult, self.workspaces_write_autopilot_objective_result) + result["ConnectorSessionAccountResult"] = from_union([lambda x: to_class(ConnectorSessionAccount, x), from_none], self.connector_session_account_result) result["SessionAuthInfoResult"] = from_union([lambda x: to_class(AuthIdentity, x), from_none], self.session_auth_info_result) result["SessionContextAttribution"] = from_union([lambda x: to_class(SessionContextAttribution, x), from_none], self.session_context_attribution) result["SessionContextInfo"] = from_union([lambda x: to_class(SessionContextInfo, x), from_none], self.session_context_info) @@ -49399,7 +52232,7 @@ def _load_AgentRegistrySpawnResult(obj: Any) -> "AgentRegistrySpawnResult": case _: raise ValueError(f"Unknown AgentRegistrySpawnResult kind: {kind!r}") # Authentication credentials accepted only at native protocol ingress. Runtime outputs use credential-free `AuthIdentity` metadata. -AuthInfo = HMACAuthInfo | EnvAuthInfo | TokenAuthInfo | TokenProviderAuthInfo | CopilotAPITokenAuthInfo | UserAuthInfo | GhCLIAuthInfo | APIKeyAuthInfo +AuthInfo = HMACAuthInfo | EnvAuthInfo | TokenAuthInfo | TokenProviderAuthInfo | CopilotAPITokenAuthInfo | UserAuthInfo | AccountAuthInfo | GhCLIAuthInfo | APIKeyAuthInfo def _load_AuthInfo(obj: Any) -> "AuthInfo": assert isinstance(obj, dict) @@ -49411,6 +52244,7 @@ def _load_AuthInfo(obj: Any) -> "AuthInfo": case "token-provider": return TokenProviderAuthInfo.from_dict(obj) case "copilot-api-token": return CopilotAPITokenAuthInfo.from_dict(obj) case "user": return UserAuthInfo.from_dict(obj) + case "account": return AccountAuthInfo.from_dict(obj) case "gh-cli": return GhCLIAuthInfo.from_dict(obj) case "api-key": return APIKeyAuthInfo.from_dict(obj) case _: raise ValueError(f"Unknown AuthInfo type: {kind!r}") @@ -49737,7 +52571,7 @@ def _load_SessionOpenParams(obj: Any) -> "SessionOpenParams": case _: raise ValueError(f"Unknown SessionOpenParams kind: {kind!r}") # Authentication credentials accepted by session.gitHubAuth.setCredentials. Session-owned token-provider identities cannot be installed through this method. -SettableAuthInfo = HMACAuthInfo | EnvAuthInfo | SettableTokenAuthInfo | CopilotAPITokenAuthInfo | UserAuthInfo | GhCLIAuthInfo | APIKeyAuthInfo +SettableAuthInfo = HMACAuthInfo | EnvAuthInfo | SettableTokenAuthInfo | CopilotAPITokenAuthInfo | UserAuthInfo | AccountAuthInfo | GhCLIAuthInfo | APIKeyAuthInfo def _load_SettableAuthInfo(obj: Any) -> "SettableAuthInfo": assert isinstance(obj, dict) @@ -49748,6 +52582,7 @@ def _load_SettableAuthInfo(obj: Any) -> "SettableAuthInfo": case "token": return SettableTokenAuthInfo.from_dict(obj) case "copilot-api-token": return CopilotAPITokenAuthInfo.from_dict(obj) case "user": return UserAuthInfo.from_dict(obj) + case "account": return AccountAuthInfo.from_dict(obj) case "gh-cli": return GhCLIAuthInfo.from_dict(obj) case "api-key": return APIKeyAuthInfo.from_dict(obj) case _: raise ValueError(f"Unknown SettableAuthInfo type: {kind!r}") @@ -49794,6 +52629,7 @@ def _load_TaskInfo(obj: Any) -> "TaskInfo": CatalogResourceVersion = str ClientMetadata = dict CommandsListRequest = Any +ConnectorSessionAccountResult = ConnectorSessionAccount | None DiagnosticCursorStatus = CursorStatus EventsCursorStatus = CursorStatus ExternalToolResult = ExternalToolTextResultForLlm @@ -49843,7 +52679,7 @@ def _load_TaskInfo(obj: Any) -> "TaskInfo": ProviderGetEndpointRequest = Any RemoteSessionMetadataTaskType = TaskType SandboxHostCapabilityName = str -SessionAuthInfoResult = AuthIdentity +SessionAuthInfoResult = AuthIdentity | None SessionCancelAllBackgroundAgentsResult = int SessionContextHostType = HostType SessionFsReaddirWithTypesEntryType = DebugCollectLogsEntryKind @@ -49892,6 +52728,27 @@ def _patch_model_capabilities(data: dict) -> dict: return data +# Experimental: this API group is experimental and may change or be removed. +class ServerEnvironmentsApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + + async def list(self, params: EnvironmentsListRequest, *, timeout: float | None = None) -> EnvironmentsListResult: + "Lists GitHub Mission Control environments visible to the authenticated identity. Does not require a running host and excludes host relay credentials.\n\nArgs:\n params: Optional discovery filters supported by GitHub Mission Control.\n\nReturns:\n Environments visible to the authenticated caller and matching the supplied filters." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return EnvironmentsListResult.from_dict(await self._client.request("environments.list", params_dict, **_timeout_kwargs(timeout))) + + async def get(self, params: EnvironmentsGetRequest, *, timeout: float | None = None) -> EnvironmentsGetResult: + "Gets safe discovery information for a GitHub Mission Control environment without requiring a running host.\n\nArgs:\n params: Identify a Mission Control environment to retrieve.\n\nReturns:\n Safe discovery information for the requested environment." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return EnvironmentsGetResult.from_dict(await self._client.request("environments.get", params_dict, **_timeout_kwargs(timeout))) + + async def delete(self, params: EnvironmentsDeleteRequest, *, timeout: float | None = None) -> EnvironmentsDeleteResult: + "Deletes a user-managed GitHub Mission Control environment. GitHub-managed environments cannot be deleted. Does not stop a running host, which may register again.\n\nArgs:\n params: Identify a user-managed Mission Control environment to delete.\n\nReturns:\n Acknowledgement that the requested environment was deleted." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return EnvironmentsDeleteResult.from_dict(await self._client.request("environments.delete", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class ServerHostApi: def __init__(self, client: "JsonRpcClient"): @@ -49903,7 +52760,7 @@ async def publish_session(self, params: HostPublishSessionRequest, *, timeout: f return HostPublishSessionResult.from_dict(await self._client.request("host.publishSession", params_dict, **_timeout_kwargs(timeout))) async def start(self, params: HostStartRequest, *, timeout: float | None = None) -> HostStartResult: - "Starts a connection-owned local AHP listener as a supervised SDK participant.\n\nArgs:\n params: Starts a supervised AHP listener in the runtime's configured working directory.\n\nReturns:\n Listener readiness, returned only after binding and the supervised participant's SDK handshake." + "Starts a connection-owned AHP host with explicit localServer and/or githubEnvironment transports as a supervised SDK participant.\n\nArgs:\n params: Starts a supervised AHP host with at least one explicitly selected transport.\n\nReturns:\n Listener readiness, returned only after binding and the supervised participant's SDK handshake." params_dict = {k: v for k, v in params.to_dict().items() if v is not None} return HostStartResult.from_dict(await self._client.request("host.start", params_dict, **_timeout_kwargs(timeout))) @@ -49939,10 +52796,41 @@ async def get_built_in_catalog(self, *, timeout: float | None = None) -> BuiltIn return BuiltInModelCatalog.from_dict(await self._client.request("models.getBuiltInCatalog", {}, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class ServerSandboxProxyCaApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + + async def get_status(self, params: SandboxProxyCARequest, *, timeout: float | None = None) -> SandboxProxyCAStatus: + "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.\n\nArgs:\n 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.\n\nReturns:\n Status of the persistent certificate authority of the sandbox credential proxy." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SandboxProxyCAStatus.from_dict(await self._client.request("sandbox.proxyCa.getStatus", params_dict, **_timeout_kwargs(timeout))) + + async def create(self, params: SandboxProxyCARequest, *, timeout: float | None = None) -> SandboxProxyCACreateResult: + "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.\n\nArgs:\n 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.\n\nReturns:\n Result of creating the persistent certificate authority of the sandbox credential proxy." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SandboxProxyCACreateResult.from_dict(await self._client.request("sandbox.proxyCa.create", params_dict, **_timeout_kwargs(timeout))) + + async def rotate(self, params: SandboxProxyCARequest, *, timeout: float | None = None) -> SandboxProxyCAStatus: + "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.\n\nArgs:\n 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.\n\nReturns:\n Status of the persistent certificate authority of the sandbox credential proxy." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SandboxProxyCAStatus.from_dict(await self._client.request("sandbox.proxyCa.rotate", params_dict, **_timeout_kwargs(timeout))) + + async def trust(self, params: SandboxProxyCARequest, *, timeout: float | None = None) -> SandboxProxyCAStatus: + "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.\n\nArgs:\n 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.\n\nReturns:\n Status of the persistent certificate authority of the sandbox credential proxy." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return SandboxProxyCAStatus.from_dict(await self._client.request("sandbox.proxyCa.trust", params_dict, **_timeout_kwargs(timeout))) + + async def remove(self, *, timeout: float | None = None) -> SandboxProxyCAStatus: + "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.\n\nReturns:\n Status of the persistent certificate authority of the sandbox credential proxy." + return SandboxProxyCAStatus.from_dict(await self._client.request("sandbox.proxyCa.remove", {}, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class ServerSandboxApi: def __init__(self, client: "JsonRpcClient"): self._client = client + self.proxy_ca = ServerSandboxProxyCaApi(client) async def get_host_support(self, *, timeout: float | None = None) -> SandboxHostSupport: "Reports whether the host running this runtime can run the command sandbox, without starting a session or spawning a sandboxed command.\n\nReturns:\n Whether the host running this runtime can run the command sandbox. The runtime checks `supported` once per process. A capability answer can change while the process runs, for example after the user installs a missing package." @@ -50380,13 +53268,32 @@ def __init__(self, client: "JsonRpcClient"): self._client = client async def read(self, *, timeout: float | None = None) -> ManagedSettingsReadResult: - "Discovers device-managed settings from production MDM and managed-file sources, validates them against the runtime-owned managed-settings schema, and returns the canonical JSON without requiring a session.\n\nReturns:\n Validated device-managed settings discovered before a session exists." + "Discovers device-managed settings from production MDM and managed-file sources, validates them against the runtime-owned managed-settings schema, and returns the canonical JSON without requiring a session. `managedSettings.resolve` returns the same device settings together with the account's server policy.\n\nReturns:\n Validated device-managed settings discovered before a session exists." return ManagedSettingsReadResult.from_dict(await self._client.request("managedSettings.read", {}, **_timeout_kwargs(timeout))) async def clear_cache(self, *, timeout: float | None = None) -> None: - "Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing \"sync account policy\" should start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed." + "Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing \"sync account policy\" should call `managedSettings.resolve` or start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed." await self._client.request("managedSettings.clearCache", {}, **_timeout_kwargs(timeout)) + async def resolve(self, params: ManagedSettingsResolveRequest, *, timeout: float | None = None) -> ManagedSettingsResolveResult: + "Resolves the effective enterprise managed settings without a session, from the device channel and, when an account is available, the account's server policy through the same per-account cache sessions use. A cached server policy less than an hour old is used without a fetch; otherwise the policy is fetched, and when the fetch fails a cached policy up to 24 hours old is used instead, unless `forceRemoteSettingsRefresh` requires a live fetch. With no account requested or signed in, it reports device policy only; signing out removes the account's cached policy. It can fetch server policy over the network when the cache is stale, so call it off latency-critical paths such as startup rather than before listing models. The policy helper is not run. `layers` lists each channel's document before merging, and `values` and `meta` carry typed effective values and their lock state for the keys typed so far.\n\nArgs:\n params: Optional opaque account selection or GitHub token whose managed settings are resolved.\n\nReturns:\n Effective enterprise managed settings for an account, resolved without a session." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return ManagedSettingsResolveResult.from_dict(await self._client.request("managedSettings.resolve", params_dict, **_timeout_kwargs(timeout))) + + async def schema(self, *, timeout: float | None = None) -> ManagedSettingsSchemaResult: + "Returns the managed-settings authoring JSON schema with descriptive `x-composition` annotations aligned with the shared settings-engine vocabulary. These annotations are not a complete runtime composition contract: model, effortLevel, and contextTier remain coupled. Use `managedSettings.compose` for the runtime's effective result. Performs no I/O.\n\nReturns:\n The authoring JSON schema for managed settings recognized by this runtime." + return ManagedSettingsSchemaResult.from_dict(await self._client.request("managedSettings.schema", {}, **_timeout_kwargs(timeout))) + + async def validate(self, params: ManagedSettingsValidateRequest, *, timeout: float | None = None) -> ManagedSettingsValidateResult: + "Validates a candidate managed-settings document the way the runtime validates delivered policy, without applying it. Reports errors that would reject the document, warnings for content the runtime ignores, and the canonical document it would apply. Document text nested more than 64 levels deep is rejected. Performs no I/O.\n\nArgs:\n params: A candidate managed-settings document to validate without applying it.\n\nReturns:\n Result of validating a managed-settings document." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return ManagedSettingsValidateResult.from_dict(await self._client.request("managedSettings.validate", params_dict, **_timeout_kwargs(timeout))) + + async def compose(self, params: ManagedSettingsComposeRequest, *, timeout: float | None = None) -> ManagedSettingsComposeResult: + "Merges candidate managed-settings documents for the device, server, and policy-helper channels into the effective settings the runtime would enforce on this host, using the same precedence and composition rules as live resolution, without applying them. Like live resolution, a server's advisory sandbox force-enable is declined on a host that cannot run the sandbox. Does not fetch policy or read policy files, but may perform blocking OS or subprocess probes for sandbox support. Preview documents are limited to 1 MiB and 64 levels of nesting.\n\nArgs:\n params: Candidate managed-settings documents to merge without applying them.\n\nReturns:\n The effective managed settings the runtime would enforce for the given documents, in the same shape `managedSettings.resolve` returns." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return ManagedSettingsComposeResult.from_dict(await self._client.request("managedSettings.compose", params_dict, **_timeout_kwargs(timeout))) + # Experimental: this API group is experimental and may change or be removed. class ServerRuntimeApi: @@ -50573,6 +53480,7 @@ class ServerRpc: """Typed server-scoped RPC methods.""" def __init__(self, client: "JsonRpcClient"): self._client = client + self.environments = ServerEnvironmentsApi(client) self.host = ServerHostApi(client) self.hooks = ServerHooksApi(client) self.models = ServerModelsApi(client) @@ -50621,6 +53529,10 @@ async def _release_session(self, params: HostSessionReleaseRequest, *, timeout: params_dict = {k: v for k, v in params.to_dict().items() if v is not None} return HostEmptyResult.from_dict(await self._client.request("host.releaseSession", params_dict, **_timeout_kwargs(timeout))) + async def _get_environment_credentials(self, *, timeout: float | None = None) -> HostEnvironmentCredentials: + "Resolves current authenticated credentials and remote-control policy only for the runtime-owned Mission Control hosting participant.\n\nReturns:\n Private credentials delivered only to a runtime-owned Mission Control hosting participant.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." + return HostEnvironmentCredentials.from_dict(await self._client.request("host.getEnvironmentCredentials", {}, **_timeout_kwargs(timeout))) + async def _get_configuration(self, *, timeout: float | None = None) -> HostConfiguration: "Returns listener settings only to the supervised hosting participant over its SDK connection.\n\nReturns:\n Normalized listener settings delivered only to the supervised hosting participant.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." return HostConfiguration.from_dict(await self._client.request("host.getConfiguration", {}, **_timeout_kwargs(timeout))) @@ -50672,24 +53584,12 @@ async def _configure_session_extensions(self, params: _ConfigureSessionExtension await self._client.request("sessions.configureSessionExtensions", params_dict, **_timeout_kwargs(timeout)) -# Experimental: this API group is experimental and may change or be removed. -class _InternalServerAccountsApi: - def __init__(self, client: "JsonRpcClient"): - self._client = client - - async def _acquire_entra_token(self, params: EntraTokenAcquireRequest, *, timeout: float | None = None) -> EntraTokenAcquireResult: - "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).\n\nArgs:\n params: OneAuth token request supplied by a trusted host application.\n\nReturns:\n Result of a OneAuth token acquisition.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} - return EntraTokenAcquireResult.from_dict(await self._client.request("accounts.acquireEntraToken", params_dict, **_timeout_kwargs(timeout))) - - class _InternalServerRpc: """Internal SDK server-scoped RPC methods. Not part of the public API.""" def __init__(self, client: "JsonRpcClient"): self._client = client self.host = _InternalServerHostApi(client) self.sessions = _InternalServerSessionsApi(client) - self.accounts = _InternalServerAccountsApi(client) async def _connect(self, params: _ConnectRequest, *, timeout: float | None = None) -> _ConnectResult: "Performs the SDK server connection handshake and validates the optional connection token. Marked internal because this is JSON-RPC transport plumbing invoked automatically by an SDK client's own `connect()` wrapper, not a user-facing method. Stays internal as long as the SDK client owns the handshake; would only become public if the SDK ever exposed the raw schema surface to consumers without a connection wrapper.\n\nArgs:\n params: Connection-level opt-ins for the `server.connect` handshake. Transport authentication is consumed by the native protocol boundary before dispatch.\n\nReturns:\n Handshake result reporting the server's protocol version and package version on success.\n\n.. warning:: This API is experimental and may change or be removed in future versions.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." @@ -50697,6 +53597,49 @@ async def _connect(self, params: _ConnectRequest, *, timeout: float | None = Non return _ConnectResult.from_dict(await self._client.request("connect", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class ProvidersModelsApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def list(self, params: ModelProviderModelsListRequest, *, timeout: float | None = None) -> DiscoveredModelList: + "Lists models installed or otherwise available from a discovered model-provider instance.\n\nArgs:\n params: Provider model inventory request parameters.\n\nReturns:\n 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." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return DiscoveredModelList.from_dict(await self._client.request("session.providers.models.list", params_dict, **_timeout_kwargs(timeout))) + + async def prepare_configuration(self, params: ModelProviderPrepareConfigurationRequest, *, timeout: float | None = None) -> ModelProviderConfigurationPlan: + "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.\n\nArgs:\n 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`.\n\nReturns:\n 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." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return ModelProviderConfigurationPlan.from_dict(await self._client.request("session.providers.models.prepareConfiguration", params_dict, **_timeout_kwargs(timeout))) + + +# Experimental: this API group is experimental and may change or be removed. +class ProvidersApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + self.models = ProvidersModelsApi(client, session_id) + + async def get_catalog(self, *, timeout: float | None = None) -> ModelProviderAdapterCatalog: + "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.\n\nReturns:\n Normalized model-provider adapter definitions available to the session, not discovered instances." + return ModelProviderAdapterCatalog.from_dict(await self._client.request("session.providers.getCatalog", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + async def discover(self, params: ModelProviderDiscoverRequest, *, timeout: float | None = None) -> ModelProviderDiscoverResult: + "Discovers reachable instances using an adapter from this session's effective provider catalog and provider-specific discovery input.\n\nArgs:\n params: Provider discovery parameters.\n\nReturns:\n Provider instances found by a discovery operation." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return ModelProviderDiscoverResult.from_dict(await self._client.request("session.providers.discover", params_dict, **_timeout_kwargs(timeout))) + + async def get_status(self, params: ModelProviderGetStatusRequest, *, timeout: float | None = None) -> ModelProviderStatus: + "Gets current health and version information for a discovered model-provider instance.\n\nArgs:\n params: Provider status request parameters.\n\nReturns:\n Current health information for a provider instance." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return ModelProviderStatus.from_dict(await self._client.request("session.providers.getStatus", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class SandboxApi: def __init__(self, client: "JsonRpcClient", session_id: str): @@ -51192,9 +54135,9 @@ async def get_sources(self, *, timeout: float | None = None) -> InstructionsGetS "Gets instruction sources loaded for the session.\n\nReturns:\n Instruction sources loaded for the session, in merge order." return InstructionsGetSourcesResult.from_dict(await self._client.request("session.instructions.getSources", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) - async def reload(self, *, timeout: float | None = None) -> None: - "Invalidates cached custom-instruction discovery so subsequent turns and source reads observe instruction files currently on disk." - await self._client.request("session.instructions.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)) + async def reload(self, *, timeout: float | None = None) -> InstructionsGetSourcesResult: + "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.\n\nReturns:\n Instruction sources loaded for the session, in merge order." + return InstructionsGetSourcesResult.from_dict(await self._client.request("session.instructions.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) # Experimental: this API group is experimental and may change or be removed. @@ -51203,9 +54146,9 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id - async def reload(self, *, timeout: float | None = None) -> SkillsLoadDiagnostics: - "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.\n\nReturns:\n Diagnostics from reloading skill definitions, with warnings and errors as separate lists." - return SkillsLoadDiagnostics.from_dict(await self._client.request("session.customizations.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + async def reload(self, *, timeout: float | None = None) -> CustomizationsReloadResult: + "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.\n\nReturns:\n 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." + return CustomizationsReloadResult.from_dict(await self._client.request("session.customizations.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) # Experimental: this API group is experimental and may change or be removed. @@ -51393,11 +54336,17 @@ async def prepare_login(self, params: SessionMCPOauthPrepareLoginRequest, *, tim return SessionMCPOauthPrepareLoginResult.from_dict(await self._client.request("session.mcp.oauth.prepareLogin", params_dict, **_timeout_kwargs(timeout))) async def login(self, params: MCPOauthLoginRequest, *, timeout: float | None = None) -> MCPOauthLoginResult: - "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.\n\nArgs:\n params: Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback success-page copy, and static OAuth client selection.\n\nReturns:\n OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server." + "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.\n\nArgs:\n params: Remote MCP server name and optional overrides controlling reauthentication, OAuth client display name, callback handling, and static OAuth client selection.\n\nReturns:\n OAuth authorization URL the caller should open, or empty when cached tokens already authenticated the server." params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id return MCPOauthLoginResult.from_dict(await self._client.request("session.mcp.oauth.login", params_dict, **_timeout_kwargs(timeout))) + async def complete(self, params: MCPOauthCompleteRequest, *, timeout: float | None = None) -> None: + "Completes a runtime-managed MCP OAuth login after the authorization server redirects to a host-managed callback URL.\n\nArgs:\n params: Host-delivered callback for a runtime-managed MCP OAuth login." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + await self._client.request("session.mcp.oauth.complete", params_dict, **_timeout_kwargs(timeout)) + async def probe(self, params: MCPOauthProbeRequest, *, timeout: float | None = None) -> MCPOauthProbeResult: "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.\n\nArgs:\n params: Remote MCP server name for a passive OAuth status probe.\n\nReturns:\n 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." params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} @@ -51496,6 +54445,25 @@ async def list_templates(self, params: MCPResourcesListTemplatesRequest, *, time return MCPResourcesListTemplatesResult.from_dict(await self._client.request("session.mcp.resources.listTemplates", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class McpPromptsApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def list(self, params: MCPPromptsListRequest, *, timeout: float | None = None) -> MCPPromptsListResult: + "Enumerate one page of prompts a connected MCP server exposes (proxies MCP `prompts/list`). Pass `cursor` to continue from a prior result's `nextCursor`.\n\nArgs:\n params: MCP server whose prompts to enumerate.\n\nReturns:\n One page of prompts advertised by the named MCP server." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return MCPPromptsListResult.from_dict(await self._client.request("session.mcp.prompts.list", params_dict, **_timeout_kwargs(timeout))) + + async def get(self, params: MCPPromptsGetRequest, *, timeout: float | None = None) -> MCPPromptsGetResult: + "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.\n\nArgs:\n params: MCP server, prompt name, and optional string-valued arguments.\n\nReturns:\n Prompt messages returned by the MCP server without sending them to the model." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return MCPPromptsGetResult.from_dict(await self._client.request("session.mcp.prompts.get", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class McpApi: def __init__(self, client: "JsonRpcClient", session_id: str): @@ -51505,11 +54473,16 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self.headers = McpHeadersApi(client, session_id) self.apps = McpAppsApi(client, session_id) self.resources = McpResourcesApi(client, session_id) + self.prompts = McpPromptsApi(client, session_id) async def list(self, *, timeout: float | None = None) -> MCPServerList: - "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.\n\nReturns:\n MCP servers configured for the session, with their connection status and host-level state." + "Lists materialized MCP servers and their connection status. Cache misses may start and wait for MCP servers.\n\nReturns:\n MCP servers configured for the session, with their connection status and host-level state." return MCPServerList.from_dict(await self._client.request("session.mcp.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + async def list_configured(self, *, timeout: float | None = None) -> MCPConfiguredServerList: + "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.\n\nReturns:\n Effective MCP configuration with optional live observations from matching already materialized servers." + return MCPConfiguredServerList.from_dict(await self._client.request("session.mcp.listConfigured", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + async def list_tools(self, params: MCPListToolsRequest, *, timeout: float | None = None) -> MCPListToolsResult: "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.\n\nArgs:\n params: Server name whose tool list should be returned.\n\nReturns:\n Tools exposed by the connected MCP server. Throws when the server is not connected." params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} @@ -51612,6 +54585,11 @@ async def get_capabilities(self, *, timeout: float | None = None) -> ConnectorCa "Returns feature availability and bounded polling limits for the EXPERIMENTAL session connector API. This method never performs a Connector service request.\n\nReturns:\n Feature detection and hard polling limits for the EXPERIMENTAL session connector API." return ConnectorCapabilities.from_dict(await self._client.request("session.connectors.getCapabilities", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + async def get_account(self, *, timeout: float | None = None) -> ConnectorSessionAccount | None: + "Returns the session account selection, or null.\n\nReturns:\n Session account selection, or null." + _result = await self._client.request("session.connectors.getAccount", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)) + return ConnectorSessionAccount.from_dict(_result) if _result is not None else None + async def get_status(self, *, timeout: float | None = None) -> ConnectorStatus: "Returns authoritative session Connector state from current availability, pinned account selection, cached catalog, and live MCP projection without performing a Connector service request.\n\nReturns:\n Authoritative session connector state. Account IDs are opaque routing identifiers and credentials are never included." return ConnectorStatus.from_dict(await self._client.request("session.connectors.getStatus", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) @@ -51666,7 +54644,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._session_id = session_id async def get(self, *, timeout: float | None = None) -> ManagedSettingsResolvedData: - "Waits for the live session's in-flight managed-settings application, then returns the retained effective snapshot used by runtime enforcement and by `session.managed_settings_resolved`. It does not perform another account, device, or server resolution, and rejects when resolution has not produced a snapshot.\n\nReturns:\n Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes." + "Waits for the live session's in-flight managed-settings application, then returns the retained effective snapshot used by runtime enforcement and by `session.managed_settings_resolved`. It does not perform another account, device, or server resolution, and rejects when resolution has not produced a snapshot.\n\nReturns:\n Effective enterprise managed settings and contributing channels. Session events report applied policy; sessionless resolve reports an account/device snapshot, and compose reports a non-applying preview of candidate documents. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively. Session-local SDK-client policy is included only in session results. Marked experimental while the managed-settings surface stabilizes." return ManagedSettingsResolvedData.from_dict(await self._client.request("session.managedSettings.get", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) @@ -51777,6 +54755,12 @@ async def sync(self, params: ProviderSyncRequest, *, timeout: float | None = Non params_dict["sessionId"] = self._session_id return ProviderSyncResult.from_dict(await self._client.request("session.provider.sync", params_dict, **_timeout_kwargs(timeout))) + async def withdraw(self, params: ProviderWithdrawRequest, *, timeout: float | None = None) -> ProviderWithdrawResult: + "Withdraws named host-managed models from the session's BYOK registry, leaving every other entry untouched. The scoped counterpart to `provider.sync`: a snapshot can only describe entries the caller knows about, so using it to remove one model silently withdraws rows registered by another source, such as a plugin calling `provider.add` at runtime. Naming what to remove leaves unrelated entries alone. Selection ids that are not registered are ignored, so withdrawal is idempotent. A provider is removed only when one of the withdrawn models was the last entry referencing it; a provider that simply has no models, which is the normal state while its rows are supplied by catalog discovery, is left in place.\n\nArgs:\n params: Host-managed model selection ids to withdraw from the session's BYOK registry.\n\nReturns:\n What the withdrawal actually removed from the registry." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return ProviderWithdrawResult.from_dict(await self._client.request("session.provider.withdraw", params_dict, **_timeout_kwargs(timeout))) + # Experimental: this API group is experimental and may change or be removed. class OptionsApi: @@ -52255,7 +55239,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._session_id = session_id async def exec(self, params: ShellExecRequest, *, timeout: float | None = None) -> ShellExecResult: - "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.\n\nArgs:\n params: Shell command to run, with optional working directory and timeout in milliseconds.\n\nReturns:\n Identifier of the spawned process, used to correlate streamed output and exit notifications." + "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.\n\nArgs:\n params: Shell command to run, with optional working directory and timeout in milliseconds. Spawn failures return an RPC error.\n\nReturns:\n Identifier of the spawned shell process, usable with shell.kill while the process is running." params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id return ShellExecResult.from_dict(await self._client.request("session.shell.exec", params_dict, **_timeout_kwargs(timeout))) @@ -52520,6 +55504,7 @@ class SessionRpc: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id + self.providers = ProvidersApi(client, session_id) self.sandbox = SandboxApi(client, session_id) self.git_hub_auth = GitHubAuthApi(client, session_id) self.accounts = AccountsApi(client, session_id) @@ -52619,7 +55604,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): async def _get_current_auth_info(self, *, timeout: float | None = None) -> AuthIdentity | None: "Gets the current authentication information for internal session hosts.\n\nReturns:\n Current authentication information, or null when no authentication is active.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." _result = await self._client.request("session.gitHubAuth.getCurrentAuthInfo", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)) - return AuthIdentity(_result) if _result is not None else None + return AuthIdentity.from_dict(_result) if _result is not None else None async def _get_all_auth_available(self, *, timeout: float | None = None) -> list: "Gets all authentication accounts available to the internal session host.\n\nReturns:\n Authentication accounts available to the internal session host.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." @@ -52628,7 +55613,7 @@ async def _get_all_auth_available(self, *, timeout: float | None = None) -> list async def _refresh_copilot_user(self, *, timeout: float | None = None) -> AuthIdentity | None: "Refreshes Copilot account metadata for the current authentication.\n\nReturns:\n Current authentication information, or null when no authentication is active.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." _result = await self._client.request("session.gitHubAuth.refreshCopilotUser", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)) - return AuthIdentity(_result) if _result is not None else None + return AuthIdentity.from_dict(_result) if _result is not None else None async def _login(self, params: SessionAuthLoginRequest, *, timeout: float | None = None) -> AuthInfo: "Logs in a GitHub user through the internal session host.\n\nArgs:\n params: Internal GitHub login parameters.\n\nReturns:\n Authentication credentials accepted only at native protocol ingress. Runtime outputs use credential-free `AuthIdentity` metadata.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." @@ -52783,6 +55768,31 @@ async def _finalize_invocation_effect(self, params: CommandsFinalizeInvocationEf return CommandsFinalizeInvocationEffectResult.from_dict(await self._client.request("session.commands.finalizeInvocationEffect", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class _InternalUiApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def _handle_human_ask_user(self, params: UIHandlePendingElicitationRequest, *, timeout: float | None = None) -> UIElicitationResult: + "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.\n\nArgs:\n params: Pending elicitation request ID and the user's response (accept/decline/cancel + form values).\n\nReturns:\n Indicates whether the elicitation response was accepted; false if it was already resolved by another client.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return UIElicitationResult.from_dict(await self._client.request("session.ui.handleHumanAskUser", params_dict, **_timeout_kwargs(timeout))) + + async def _handle_human_user_input(self, params: UIHandlePendingUserInputRequest, *, timeout: float | None = None) -> UIHandlePendingResult: + "Resolves a pending `user_input.requested` event after direct interaction in the trusted in-process client.\n\nArgs:\n params: Request ID of a pending `user_input.requested` event and the user's response.\n\nReturns:\n Indicates whether the pending UI request was resolved by this call.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return UIHandlePendingResult.from_dict(await self._client.request("session.ui.handleHumanUserInput", params_dict, **_timeout_kwargs(timeout))) + + async def _handle_human_exit_plan_mode(self, params: UIHandlePendingExitPlanModeRequest, *, timeout: float | None = None) -> UIHandlePendingResult: + "Resolves a pending `exit_plan_mode.requested` event after direct interaction in the trusted in-process client.\n\nArgs:\n params: Request ID of a pending `exit_plan_mode.requested` event and the user's response.\n\nReturns:\n Indicates whether the pending UI request was resolved by this call.\n\n:meta private:\n\nInternal SDK API; not part of the public surface." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return UIHandlePendingResult.from_dict(await self._client.request("session.ui.handleHumanExitPlanMode", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class _InternalSettingsApi: def __init__(self, client: "JsonRpcClient", session_id: str): @@ -52904,6 +55914,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self.mcp = _InternalMcpApi(client, session_id) self.connectors = _InternalConnectorsApi(client, session_id) self.commands = _InternalCommandsApi(client, session_id) + self.ui = _InternalUiApi(client, session_id) self.settings = _InternalSettingsApi(client, session_id) self.queue = _InternalQueueApi(client, session_id) self.schedule = _InternalScheduleApi(client, session_id) @@ -52941,9 +55952,15 @@ class SessionFsHandler(Protocol): async def read_file(self, params: SessionFSReadFileRequest) -> SessionFSReadFileResult: "Reads a file from the client-provided session filesystem.\n\nArgs:\n params: Path of the file to read from the client-provided session filesystem.\n\nReturns:\n File content as a UTF-8 string, or a filesystem error if the read failed." pass + async def read_file_bytes(self, params: SessionFSReadFileBytesRequest) -> SessionFSReadFileBytesResult: + "Reads binary file content from the client-provided session filesystem.\n\nArgs:\n params: Path of the binary file to read from the client-provided session filesystem.\n\nReturns:\n File bytes as standard base64, or a filesystem error if the read failed." + pass async def write_file(self, params: SessionFSWriteFileRequest) -> SessionFSError | None: "Writes a file in the client-provided session filesystem.\n\nArgs:\n params: File path, content to write, and optional mode for the client-provided session filesystem.\n\nReturns:\n Describes a filesystem error." pass + async def write_file_bytes(self, params: SessionFSWriteFileBytesRequest) -> SessionFSError | None: + "Writes binary file content to the client-provided session filesystem.\n\nArgs:\n params: File path, standard-base64-encoded bytes to write, and optional mode for the client-provided session filesystem.\n\nReturns:\n Describes a filesystem error." + pass async def append_file(self, params: SessionFSAppendFileRequest) -> SessionFSError | None: "Appends content to a file in the client-provided session filesystem, creating parent directories as needed.\n\nArgs:\n params: File path, content to append, and optional mode for the client-provided session filesystem. Implementations create parent directories as needed.\n\nReturns:\n Describes a filesystem error." pass @@ -53038,6 +56055,13 @@ async def handle_session_fs_read_file(params: dict) -> dict | None: result = await handler.read_file(request) return result.to_dict() client.set_request_handler("sessionFs.readFile", handle_session_fs_read_file) + async def handle_session_fs_read_file_bytes(params: dict) -> dict | None: + request = SessionFSReadFileBytesRequest.from_dict(params) + handler = get_handlers(request.session_id).session_fs + if handler is None: raise RuntimeError(f"No session_fs handler registered for session: {request.session_id}") + result = await handler.read_file_bytes(request) + return result.to_dict() + client.set_request_handler("sessionFs.readFileBytes", handle_session_fs_read_file_bytes) async def handle_session_fs_write_file(params: dict) -> dict | None: request = SessionFSWriteFileRequest.from_dict(params) handler = get_handlers(request.session_id).session_fs @@ -53045,6 +56069,13 @@ async def handle_session_fs_write_file(params: dict) -> dict | None: result = await handler.write_file(request) return result.to_dict() if result is not None else None client.set_request_handler("sessionFs.writeFile", handle_session_fs_write_file) + async def handle_session_fs_write_file_bytes(params: dict) -> dict | None: + request = SessionFSWriteFileBytesRequest.from_dict(params) + handler = get_handlers(request.session_id).session_fs + if handler is None: raise RuntimeError(f"No session_fs handler registered for session: {request.session_id}") + result = await handler.write_file_bytes(request) + return result.to_dict() if result is not None else None + client.set_request_handler("sessionFs.writeFileBytes", handle_session_fs_write_file_bytes) async def handle_session_fs_append_file(params: dict) -> dict | None: request = SessionFSAppendFileRequest.from_dict(params) handler = get_handlers(request.session_id).session_fs @@ -53318,6 +56349,8 @@ async def handle_installations_confirm(params: dict) -> dict | None: "AbortResult", "AcceptedEnqueueCommandResult", "AccountAllUsers", + "AccountAuthInfo", + "AccountAuthInfoType", "AccountGetAllUsersResult", "AccountGetCurrentAuthResult", "AccountGetQuotaRequest", @@ -53378,8 +56411,10 @@ async def handle_installations_confirm(params: dict) -> dict | None: "AuthEnumerateQueryKind", "AuthEnumerateValue", "AuthIdentity", + "AuthIdentityMetadata", "AuthInfo", "AuthInfoType", + "AuthLoginAccount", "AuthLoginAdvanceRequest", "AuthLoginBeginRequest", "AuthLoginBegun", @@ -53603,6 +56638,8 @@ async def handle_installations_confirm(params: dict) -> dict | None: "ConnectorMCPStatus", "ConnectorReconcileRequest", "ConnectorRuntimeStatus", + "ConnectorSessionAccount", + "ConnectorSessionAccountResult", "ConnectorStatus", "ConnectorsApi", "ContentExclusionApi", @@ -53622,7 +56659,11 @@ async def handle_installations_confirm(params: dict) -> dict | None: "CurrentModel", "CurrentToolMetadata", "CursorStatus", + "CustomizationReloadOutcome", + "CustomizationReloadStatus", + "CustomizationReloadSubsystem", "CustomizationsApi", + "CustomizationsReloadResult", "DebugApi", "DebugCollectLogsCollectedEntry", "DebugCollectLogsDestination", @@ -53657,16 +56698,22 @@ async def handle_installations_confirm(params: dict) -> dict | None: "DiscoveredHook", "DiscoveredMCPServer", "DiscoveredMCPServerType", + "DiscoveredModel", + "DiscoveredModelList", "EnqueueCommandParams", "EnqueueCommandResult", "EnterpriseList", - "EntraTokenAcquireRequest", - "EntraTokenAcquireResult", - "EntraTokenAcquireResultStatus", - "EntraTokenInteraction", "Entry", "EnvAuthInfo", "EnvAuthInfoType", + "EnvironmentCapabilities", + "EnvironmentKind", + "EnvironmentsDeleteRequest", + "EnvironmentsDeleteResult", + "EnvironmentsGetRequest", + "EnvironmentsGetResult", + "EnvironmentsListRequest", + "EnvironmentsListResult", "EventLogApi", "EventLogReadRequest", "EventLogReleaseInterestResult", @@ -53724,6 +56771,7 @@ async def handle_installations_confirm(params: dict) -> dict | None: "GhCLIAuthInfo", "GhCLIAuthInfoType", "GitHubAuthApi", + "GitHubEnvironment", "GitHubTelemetryClientInfo", "GitHubTelemetryEvent", "GitHubTelemetryHandler", @@ -53770,9 +56818,13 @@ async def handle_installations_confirm(params: dict) -> dict | None: "HostConfiguration", "HostDisposeRequest", "HostEmptyResult", + "HostEnvironmentCredentials", "HostExitReason", "HostExitedNotification", + "HostGitHubEnvironmentOptions", "HostHandler", + "HostLocalServerConfiguration", + "HostLocalServerOptions", "HostPublishSessionRequest", "HostPublishSessionResult", "HostReadyRequest", @@ -53865,6 +56917,9 @@ async def handle_installations_confirm(params: dict) -> dict | None: "MCPConfigUpdateRequest", "MCPConfigureGitHubRequest", "MCPConfigureGitHubResult", + "MCPConfiguredServer", + "MCPConfiguredServerList", + "MCPConfiguredServerState", "MCPDiagnosticDetails", "MCPDiagnosticDirection", "MCPDiagnosticKind", @@ -53910,6 +56965,7 @@ async def handle_installations_confirm(params: dict) -> dict | None: "MCPOauthAuthenticationStateChangedRequest", "MCPOauthCancelLoginRequest", "MCPOauthCancelLoginResult", + "MCPOauthCompleteRequest", "MCPOauthHandlePendingRequest", "MCPOauthHandlePendingResult", "MCPOauthLoginRequest", @@ -53967,6 +57023,15 @@ async def handle_installations_confirm(params: dict) -> dict | None: "MCPPlanValueCategory", "MCPPrepareInstallRequest", "MCPPreparedInstall", + "MCPPrompt", + "MCPPromptArgument", + "MCPPromptIcon", + "MCPPromptMessage", + "MCPPromptRole", + "MCPPromptsGetRequest", + "MCPPromptsGetResult", + "MCPPromptsListRequest", + "MCPPromptsListResult", "MCPRegisterExternalClientRequest", "MCPReloadConfig", "MCPReloadWithConfigRequest", @@ -54025,9 +57090,24 @@ async def handle_installations_confirm(params: dict) -> dict | None: "MCPUninstallPlan", "MCPUnregisterExternalClientRequest", "ManagedMCPServerConfig", + "ManagedSettingMeta", "ManagedSettingsApi", + "ManagedSettingsChannel", + "ManagedSettingsComposeLayer", + "ManagedSettingsComposeRequest", + "ManagedSettingsComposeResult", + "ManagedSettingsDiagnostic", + "ManagedSettingsDiagnosticSeverity", + "ManagedSettingsLayer", + "ManagedSettingsMeta", "ManagedSettingsReadResult", + "ManagedSettingsResolveRequest", + "ManagedSettingsResolveResult", "ManagedSettingsResolvedData", + "ManagedSettingsSchemaResult", + "ManagedSettingsValidateRequest", + "ManagedSettingsValidateResult", + "ManagedSettingsValues", "MarketplaceAddResult", "MarketplaceBrowseResult", "MarketplaceInfo", @@ -54058,6 +57138,7 @@ async def handle_installations_confirm(params: dict) -> dict | None: "McpPlanScalarValueType", "McpPlanSecretReference", "McpPlanTransportChoice", + "McpPromptsApi", "McpResourcesApi", "McpSafeForTelemetry", "McpServerAuthConfig", @@ -54088,6 +57169,7 @@ async def handle_installations_confirm(params: dict) -> dict | None: "Model", "ModelApi", "ModelApplyStartupOverlayRequest", + "ModelArtifactDetails", "ModelBilling", "ModelBillingPromo", "ModelBillingTokenPrices", @@ -54109,9 +57191,31 @@ async def handle_installations_confirm(params: dict) -> dict | None: "ModelPickerSettingsContext", "ModelPolicy", "ModelPolicyState", + "ModelProviderAdapterCatalog", + "ModelProviderAdapterDescriptor", + "ModelProviderAdapterOperationDescriptor", + "ModelProviderAttribution", + "ModelProviderAutomaticDiscoveryMode", + "ModelProviderAutomaticDiscoveryPolicy", + "ModelProviderConfigurationDisposition", + "ModelProviderConfigurationPlan", "ModelProviderDescriptor", + "ModelProviderDiscoverRequest", + "ModelProviderDiscoverResult", + "ModelProviderDiscoveryNetworkScope", + "ModelProviderGetStatusRequest", + "ModelProviderInstance", + "ModelProviderInstanceReference", "ModelProviderKind", + "ModelProviderModelsListRequest", + "ModelProviderOperationOutcome", + "ModelProviderOperationOutcomeCode", + "ModelProviderPrepareConfigurationRequest", + "ModelProviderProvenance", + "ModelProviderProvenanceSource", "ModelProviderRef", + "ModelProviderStatus", + "ModelProviderWarning", "ModelSetAllowedModelsRequest", "ModelSetAllowedModelsResult", "ModelSetReasoningEffortRequest", @@ -54335,6 +57439,7 @@ async def handle_installations_confirm(params: dict) -> dict | None: "ProviderApi", "ProviderConfig", "ProviderConfigAzure", + "ProviderConfigModelProvider", "ProviderConfigTransport", "ProviderConfigType", "ProviderConfigWireApi", @@ -54354,6 +57459,10 @@ async def handle_installations_confirm(params: dict) -> dict | None: "ProviderTransport", "ProviderType", "ProviderWireAPI", + "ProviderWithdrawRequest", + "ProviderWithdrawResult", + "ProvidersApi", + "ProvidersModelsApi", "PurpleSource", "PushAttachment", "PushAttachmentBlob", @@ -54482,6 +57591,10 @@ async def handle_installations_confirm(params: dict) -> dict | None: "SandboxHostCapabilityName", "SandboxHostSupport", "SandboxMaskedEnvVar", + "SandboxProxyCACreateResult", + "SandboxProxyCARequest", + "SandboxProxyCAState", + "SandboxProxyCAStatus", "SandboxSessionChange", "Saved", "ScheduleAddAtRequest", @@ -54513,6 +57626,7 @@ async def handle_installations_confirm(params: dict) -> dict | None: "ServerAgentsApi", "ServerCatalogApi", "ServerCommandsApi", + "ServerEnvironmentsApi", "ServerExtensionsApi", "ServerHooksApi", "ServerHostApi", @@ -54530,6 +57644,7 @@ async def handle_installations_confirm(params: dict) -> dict | None: "ServerRpc", "ServerRuntimeApi", "ServerSandboxApi", + "ServerSandboxProxyCaApi", "ServerSecretsApi", "ServerSessionFsApi", "ServerSessionsApi", @@ -54564,6 +57679,8 @@ async def handle_installations_confirm(params: dict) -> dict | None: "SessionFSExistsRequest", "SessionFSExistsResult", "SessionFSMkdirRequest", + "SessionFSReadFileBytesRequest", + "SessionFSReadFileBytesResult", "SessionFSReadFileRequest", "SessionFSReadFileResult", "SessionFSReaddirRequest", @@ -54589,6 +57706,7 @@ async def handle_installations_confirm(params: dict) -> dict | None: "SessionFSSqliteTransactionStatement", "SessionFSStatRequest", "SessionFSStatResult", + "SessionFSWriteFileBytesRequest", "SessionFSWriteFileRequest", "SessionFsHandler", "SessionFsReaddirWithTypesEntryType", diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index cc4d73fe58..502455bedf 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -198,12 +198,14 @@ class SessionEventType(Enum): ASSISTANT_USAGE = "assistant.usage" PROMPT_CACHE_BREAK = "prompt_cache_break" MODEL_CALL_FAILURE = "model.call_failure" + MODEL_CALL_FINAL_RESULT = "model.call_final_result" MODEL_CALL_FINISHED = "model.call_finished" MODEL_CALL_START = "model.call_start" ABORT = "abort" TOOL_USER_REQUESTED = "tool.user_requested" TOOL_EXECUTION_START = "tool.execution_start" TOOL_EXECUTION_PARTIAL_RESULT = "tool.execution_partial_result" + TOOL_SHELL_OUTPUT = "tool.shell_output" TOOL_EXECUTION_PROGRESS = "tool.execution_progress" TOOL_EXECUTION_COMPLETE = "tool.execution_complete" TOOL_SEARCH_ACTIVATED = "tool_search.activated" @@ -274,6 +276,7 @@ class SessionEventType(Enum): CAPABILITIES_CHANGED = "capabilities.changed" EXIT_PLAN_MODE_REQUESTED = "exit_plan_mode.requested" EXIT_PLAN_MODE_COMPLETED = "exit_plan_mode.completed" + HUMAN_RESPONSE_RECORDED = "human_response.recorded" SESSION_TOOLS_UPDATED = "session.tools_updated" SESSION_BACKGROUND_TASKS_CHANGED = "session.background_tasks_changed" # Experimental: this event is part of an experimental API and may change or be removed. @@ -428,6 +431,30 @@ def to_dict(self) -> dict: return result +# Deprecated: this type is deprecated and will be removed in a future version. +@dataclass +class ToolExecutionPartialResultData: + "Deprecated merged replacement snapshot of shell output. Use tool.shell_output for append-only, stream-tagged output instead." + partial_output: str + tool_call_id: str + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionPartialResultData": + assert isinstance(obj, dict) + partial_output = from_str(obj.get("partialOutput")) + tool_call_id = from_str(obj.get("toolCallId")) + return ToolExecutionPartialResultData( + partial_output=partial_output, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["partialOutput"] = from_str(self.partial_output) + result["toolCallId"] = from_str(self.tool_call_id) + return result + + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AssistantFusionPhaseActivityData: @@ -694,15 +721,18 @@ class AssistantMessageReasoningBlocks: "Neutral provider-tagged reasoning content blocks preserved verbatim for round-tripping" provider: str blocks: list[Any] | None = None + ordered_blocks: list[Any] | None = None @staticmethod def from_dict(obj: Any) -> "AssistantMessageReasoningBlocks": assert isinstance(obj, dict) provider = from_str(obj.get("provider")) blocks = from_union([from_none, lambda x: from_list(lambda x: x, x)], obj.get("blocks")) + ordered_blocks = from_union([from_none, lambda x: from_list(lambda x: x, x)], obj.get("orderedBlocks")) return AssistantMessageReasoningBlocks( provider=provider, blocks=blocks, + ordered_blocks=ordered_blocks, ) def to_dict(self) -> dict: @@ -710,6 +740,8 @@ def to_dict(self) -> dict: result["provider"] = from_str(self.provider) if self.blocks is not None: result["blocks"] = from_union([from_none, lambda x: from_list(lambda x: x, x)], self.blocks) + if self.ordered_blocks is not None: + result["orderedBlocks"] = from_union([from_none, lambda x: from_list(lambda x: x, x)], self.ordered_blocks) return result @@ -1742,6 +1774,7 @@ class SessionAutoModeResolvedData: reasoning_bucket: AutoModeResolvedReasoningBucket | None = None router_latency_ms: float | None = None routing_method: str | None = None + selection_reason: str | None = None sticky_override: bool | None = None @staticmethod @@ -1761,6 +1794,7 @@ def from_dict(obj: Any) -> "SessionAutoModeResolvedData": reasoning_bucket = from_union([from_none, lambda x: parse_enum(AutoModeResolvedReasoningBucket, x)], obj.get("reasoningBucket")) router_latency_ms = from_union([from_none, from_float], obj.get("routerLatencyMs")) routing_method = from_union([from_none, from_str], obj.get("routingMethod")) + selection_reason = from_union([from_none, from_str], obj.get("selectionReason")) sticky_override = from_union([from_none, from_bool], obj.get("stickyOverride")) return SessionAutoModeResolvedData( chosen_model=chosen_model, @@ -1777,6 +1811,7 @@ def from_dict(obj: Any) -> "SessionAutoModeResolvedData": reasoning_bucket=reasoning_bucket, router_latency_ms=router_latency_ms, routing_method=routing_method, + selection_reason=selection_reason, sticky_override=sticky_override, ) @@ -1809,6 +1844,8 @@ def to_dict(self) -> dict: result["routerLatencyMs"] = from_union([from_none, to_float], self.router_latency_ms) if self.routing_method is not None: result["routingMethod"] = from_union([from_none, from_str], self.routing_method) + if self.selection_reason is not None: + result["selectionReason"] = from_union([from_none, from_str], self.selection_reason) if self.sticky_override is not None: result["stickyOverride"] = from_union([from_none, from_bool], self.sticky_override) return result @@ -2428,7 +2465,7 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionManagedSettingsResolvedData: - "Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes." + "Effective enterprise managed settings and contributing channels. Session events report applied policy; sessionless resolve reports an account/device snapshot, and compose reports a non-applying preview of candidate documents. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively. Session-local SDK-client policy is included only in session results. Marked experimental while the managed-settings surface stabilizes." bypass_permissions_disabled: bool device_managed: bool fail_closed: bool @@ -2556,6 +2593,30 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ToolExecutionCompleteFileEdit: + "A file mutation that was actually committed by a built-in file editing tool." + kind: ToolExecutionCompleteFileEditKind + path: str + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionCompleteFileEdit": + assert isinstance(obj, dict) + kind = parse_enum(ToolExecutionCompleteFileEditKind, obj.get("kind")) + path = from_str(obj.get("path")) + return ToolExecutionCompleteFileEdit( + kind=kind, + path=path, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(ToolExecutionCompleteFileEditKind, self.kind) + result["path"] = from_str(self.path) + return result + + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ToolExecutionCompleteShellExecution: @@ -3415,6 +3476,7 @@ class AssistantUsageData: api_endpoint: AssistantUsageApiEndpoint | None = None # Internal: this field is an internal SDK API and is not part of the public surface. _available_tool_count: int | None = None + byok_kind: str | None = None # Internal: this field is an internal SDK API and is not part of the public surface. _cache_details_reported: bool | None = None cache_expires_at: datetime | None = None @@ -3440,6 +3502,7 @@ class AssistantUsageData: is_byok: bool | None = None max_output_tokens: int | None = None max_prompt_tokens: int | None = None + model_provider: str | None = None # Internal: this field is an internal SDK API and is not part of the public surface. _num_tool_calls: int | None = None output_tokens: int | None = None @@ -3474,6 +3537,7 @@ def from_dict(obj: Any) -> "AssistantUsageData": api_call_id = from_union([from_none, from_str], obj.get("apiCallId")) api_endpoint = from_union([from_none, lambda x: parse_enum(AssistantUsageApiEndpoint, x)], obj.get("apiEndpoint")) _available_tool_count = from_union([from_none, from_int], obj.get("availableToolCount")) + byok_kind = from_union([from_none, from_str], obj.get("byokKind")) _cache_details_reported = from_union([from_none, from_bool], obj.get("cacheDetailsReported")) cache_expires_at = from_union([from_none, from_datetime], obj.get("cacheExpiresAt")) cache_read_tokens = from_union([from_none, from_int], obj.get("cacheReadTokens")) @@ -3494,6 +3558,7 @@ def from_dict(obj: Any) -> "AssistantUsageData": is_byok = from_union([from_none, from_bool], obj.get("isByok")) max_output_tokens = from_union([from_none, from_int], obj.get("maxOutputTokens")) max_prompt_tokens = from_union([from_none, from_int], obj.get("maxPromptTokens")) + model_provider = from_union([from_none, from_str], obj.get("modelProvider")) _num_tool_calls = from_union([from_none, from_int], obj.get("numToolCalls")) output_tokens = from_union([from_none, from_int], obj.get("outputTokens")) output_ttft = from_union([from_none, from_timedelta], obj.get("outputTtftMs")) @@ -3518,6 +3583,7 @@ def from_dict(obj: Any) -> "AssistantUsageData": api_call_id=api_call_id, api_endpoint=api_endpoint, _available_tool_count=_available_tool_count, + byok_kind=byok_kind, _cache_details_reported=_cache_details_reported, cache_expires_at=cache_expires_at, cache_read_tokens=cache_read_tokens, @@ -3538,6 +3604,7 @@ def from_dict(obj: Any) -> "AssistantUsageData": is_byok=is_byok, max_output_tokens=max_output_tokens, max_prompt_tokens=max_prompt_tokens, + model_provider=model_provider, _num_tool_calls=_num_tool_calls, output_tokens=output_tokens, output_ttft=output_ttft, @@ -3569,6 +3636,8 @@ def to_dict(self) -> dict: result["apiEndpoint"] = from_union([from_none, lambda x: to_enum(AssistantUsageApiEndpoint, x)], self.api_endpoint) if self._available_tool_count is not None: result["availableToolCount"] = from_union([from_none, to_int], self._available_tool_count) + if self.byok_kind is not None: + result["byokKind"] = from_union([from_none, from_str], self.byok_kind) if self._cache_details_reported is not None: result["cacheDetailsReported"] = from_union([from_none, from_bool], self._cache_details_reported) if self.cache_expires_at is not None: @@ -3609,6 +3678,8 @@ def to_dict(self) -> dict: result["maxOutputTokens"] = from_union([from_none, to_int], self.max_output_tokens) if self.max_prompt_tokens is not None: result["maxPromptTokens"] = from_union([from_none, to_int], self.max_prompt_tokens) + if self.model_provider is not None: + result["modelProvider"] = from_union([from_none, from_str], self.model_provider) if self._num_tool_calls is not None: result["numToolCalls"] = from_union([from_none, to_int], self._num_tool_calls) if self.output_tokens is not None: @@ -5446,6 +5517,137 @@ def to_dict(self) -> dict: return result +@dataclass +class HumanResponseRecordedData: + "Durable request-correlated evidence for a typed response to a runtime-owned question or plan review." + actor: HumanResponseActor + request_id: str + response: HumanResponseRecordedResponse + tool_call_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "HumanResponseRecordedData": + assert isinstance(obj, dict) + actor = parse_enum(HumanResponseActor, obj.get("actor")) + request_id = from_str(obj.get("requestId")) + response = HumanResponseRecordedResponse.from_dict(obj.get("response")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + return HumanResponseRecordedData( + actor=actor, + request_id=request_id, + response=response, + tool_call_id=tool_call_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["actor"] = to_enum(HumanResponseActor, self.actor) + result["requestId"] = from_str(self.request_id) + result["response"] = to_class(HumanResponseRecordedResponse, self.response) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + return result + + +@dataclass +class HumanResponseRecordedResponse: + "Exact runtime-owned question or reviewed plan paired with the typed response that settled it." + response_kind: HumanResponseRecordedResponseResponseKind + actions: list[ExitPlanModeAction] | None = None + allow_freeform: bool | None = None + answer: str | None = None + approved: bool | None = None + auto_approve_edits: bool | None = None + choices: list[str] | None = None + content: dict[str, Any] | None = None + feedback: str | None = None + message: str | None = None + plan_content: str | None = None + question: str | None = None + recommended_action: ExitPlanModeAction | None = None + requested_schema: ElicitationRequestedSchema | None = None + selected_action: ExitPlanModeAction | None = None + summary: str | None = None + was_freeform: bool | None = None + + @staticmethod + def from_dict(obj: Any) -> "HumanResponseRecordedResponse": + assert isinstance(obj, dict) + response_kind = parse_enum(HumanResponseRecordedResponseResponseKind, obj.get("responseKind")) + actions = from_union([from_none, lambda x: from_list(lambda x: parse_enum(ExitPlanModeAction, x), x)], obj.get("actions")) + allow_freeform = from_union([from_none, from_bool], obj.get("allowFreeform")) + answer = from_union([from_none, from_str], obj.get("answer")) + approved = from_union([from_none, from_bool], obj.get("approved")) + auto_approve_edits = from_union([from_none, from_bool], obj.get("autoApproveEdits")) + choices = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("choices")) + content = from_union([from_none, lambda x: from_dict(lambda x: x, x)], obj.get("content")) + feedback = from_union([from_none, from_str], obj.get("feedback")) + message = from_union([from_none, from_str], obj.get("message")) + plan_content = from_union([from_none, from_str], obj.get("planContent")) + question = from_union([from_none, from_str], obj.get("question")) + recommended_action = from_union([from_none, lambda x: parse_enum(ExitPlanModeAction, x)], obj.get("recommendedAction")) + requested_schema = from_union([from_none, ElicitationRequestedSchema.from_dict], obj.get("requestedSchema")) + selected_action = from_union([from_none, lambda x: parse_enum(ExitPlanModeAction, x)], obj.get("selectedAction")) + summary = from_union([from_none, from_str], obj.get("summary")) + was_freeform = from_union([from_none, from_bool], obj.get("wasFreeform")) + return HumanResponseRecordedResponse( + response_kind=response_kind, + actions=actions, + allow_freeform=allow_freeform, + answer=answer, + approved=approved, + auto_approve_edits=auto_approve_edits, + choices=choices, + content=content, + feedback=feedback, + message=message, + plan_content=plan_content, + question=question, + recommended_action=recommended_action, + requested_schema=requested_schema, + selected_action=selected_action, + summary=summary, + was_freeform=was_freeform, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["responseKind"] = to_enum(HumanResponseRecordedResponseResponseKind, self.response_kind) + if self.actions is not None: + result["actions"] = from_union([from_none, lambda x: from_list(lambda x: to_enum(ExitPlanModeAction, x), x)], self.actions) + if self.allow_freeform is not None: + result["allowFreeform"] = from_union([from_none, from_bool], self.allow_freeform) + if self.answer is not None: + result["answer"] = from_union([from_none, from_str], self.answer) + if self.approved is not None: + result["approved"] = from_union([from_none, from_bool], self.approved) + if self.auto_approve_edits is not None: + result["autoApproveEdits"] = from_union([from_none, from_bool], self.auto_approve_edits) + if self.choices is not None: + result["choices"] = from_union([from_none, lambda x: from_list(from_str, x)], self.choices) + if self.content is not None: + result["content"] = from_union([from_none, lambda x: from_dict(lambda x: x, x)], self.content) + if self.feedback is not None: + result["feedback"] = from_union([from_none, from_str], self.feedback) + if self.message is not None: + result["message"] = from_union([from_none, from_str], self.message) + if self.plan_content is not None: + result["planContent"] = from_union([from_none, from_str], self.plan_content) + if self.question is not None: + result["question"] = from_union([from_none, from_str], self.question) + if self.recommended_action is not None: + result["recommendedAction"] = from_union([from_none, lambda x: to_enum(ExitPlanModeAction, x)], self.recommended_action) + if self.requested_schema is not None: + result["requestedSchema"] = from_union([from_none, lambda x: to_class(ElicitationRequestedSchema, x)], self.requested_schema) + if self.selected_action is not None: + result["selectedAction"] = from_union([from_none, lambda x: to_enum(ExitPlanModeAction, x)], self.selected_action) + if self.summary is not None: + result["summary"] = from_union([from_none, from_str], self.summary) + if self.was_freeform is not None: + result["wasFreeform"] = from_union([from_none, from_bool], self.was_freeform) + return result + + @dataclass class McpAppToolCallCompleteData: "MCP App view called a tool on a connected MCP server (SEP-1865)" @@ -5927,6 +6129,7 @@ class ModelCallFailureData: api_call_id: str | None = None api_endpoint: AssistantUsageApiEndpoint | None = None bad_request_kind: ModelCallFailureBadRequestKind | None = None + byok_kind: str | None = None duration: timedelta | None = None error_code: str | None = None error_message: str | None = None @@ -5941,6 +6144,7 @@ class ModelCallFailureData: max_output_tokens: int | None = None max_prompt_tokens: int | None = None model: str | None = None + model_provider: str | None = None parent_tool_call_id: str | None = None provider_call_id: str | None = None # Internal: this field is an internal SDK API and is not part of the public surface. @@ -5959,6 +6163,7 @@ def from_dict(obj: Any) -> "ModelCallFailureData": api_call_id = from_union([from_none, from_str], obj.get("apiCallId")) api_endpoint = from_union([from_none, lambda x: parse_enum(AssistantUsageApiEndpoint, x)], obj.get("apiEndpoint")) bad_request_kind = from_union([from_none, lambda x: parse_enum(ModelCallFailureBadRequestKind, x)], obj.get("badRequestKind")) + byok_kind = from_union([from_none, from_str], obj.get("byokKind")) duration = from_union([from_none, from_timedelta], obj.get("durationMs")) error_code = from_union([from_none, from_str], obj.get("errorCode")) error_message = from_union([from_none, from_str], obj.get("errorMessage")) @@ -5972,6 +6177,7 @@ def from_dict(obj: Any) -> "ModelCallFailureData": max_output_tokens = from_union([from_none, from_int], obj.get("maxOutputTokens")) max_prompt_tokens = from_union([from_none, from_int], obj.get("maxPromptTokens")) model = from_union([from_none, from_str], obj.get("model")) + model_provider = from_union([from_none, from_str], obj.get("modelProvider")) parent_tool_call_id = from_union([from_none, from_str], obj.get("parentToolCallId")) provider_call_id = from_union([from_none, from_str], obj.get("providerCallId")) _quota_snapshots = from_union([from_none, lambda x: from_dict(_AssistantUsageQuotaSnapshot.from_dict, x)], obj.get("quotaSnapshots")) @@ -5986,6 +6192,7 @@ def from_dict(obj: Any) -> "ModelCallFailureData": api_call_id=api_call_id, api_endpoint=api_endpoint, bad_request_kind=bad_request_kind, + byok_kind=byok_kind, duration=duration, error_code=error_code, error_message=error_message, @@ -5999,6 +6206,7 @@ def from_dict(obj: Any) -> "ModelCallFailureData": max_output_tokens=max_output_tokens, max_prompt_tokens=max_prompt_tokens, model=model, + model_provider=model_provider, parent_tool_call_id=parent_tool_call_id, provider_call_id=provider_call_id, _quota_snapshots=_quota_snapshots, @@ -6019,6 +6227,8 @@ def to_dict(self) -> dict: result["apiEndpoint"] = from_union([from_none, lambda x: to_enum(AssistantUsageApiEndpoint, x)], self.api_endpoint) if self.bad_request_kind is not None: result["badRequestKind"] = from_union([from_none, lambda x: to_enum(ModelCallFailureBadRequestKind, x)], self.bad_request_kind) + if self.byok_kind is not None: + result["byokKind"] = from_union([from_none, from_str], self.byok_kind) if self.duration is not None: result["durationMs"] = from_union([from_none, to_timedelta_int], self.duration) if self.error_code is not None: @@ -6045,6 +6255,8 @@ def to_dict(self) -> dict: result["maxPromptTokens"] = from_union([from_none, to_int], self.max_prompt_tokens) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) + if self.model_provider is not None: + result["modelProvider"] = from_union([from_none, from_str], self.model_provider) if self.parent_tool_call_id is not None: result["parentToolCallId"] = from_union([from_none, from_str], self.parent_tool_call_id) if self.provider_call_id is not None: @@ -6110,6 +6322,34 @@ def to_dict(self) -> dict: return result +@dataclass +class ModelCallFinalResultData: + "Internal telemetry result for one logical model operation after all orchestrator-owned retries settle" + model: str + result: ModelCallFinalResult + is_byok: bool | None = None + + @staticmethod + def from_dict(obj: Any) -> "ModelCallFinalResultData": + assert isinstance(obj, dict) + model = from_str(obj.get("model")) + result = parse_enum(ModelCallFinalResult, obj.get("result")) + is_byok = from_union([from_none, from_bool], obj.get("isByok")) + return ModelCallFinalResultData( + model=model, + result=result, + is_byok=is_byok, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["model"] = from_str(self.model) + result["result"] = to_enum(ModelCallFinalResult, self.result) + if self.is_byok is not None: + result["isByok"] = from_union([from_none, from_bool], self.is_byok) + return result + + @dataclass class ModelCallFinishedData: "Final lifecycle outcome for one logical model dispatch. A logical dispatch may include internal reconnect or fallback work, so event count is not provider HTTP-request count." @@ -8184,6 +8424,8 @@ class PromptCacheBreakData: # Internal: this field is an internal SDK API and is not part of the public surface. _tools_redefined: list[str] | None = None # Internal: this field is an internal SDK API and is not part of the public surface. + _tools_redefined_parts: list[str] | None = None + # Internal: this field is an internal SDK API and is not part of the public surface. _tools_redefined_raw: list[str] | None = None # Internal: this field is an internal SDK API and is not part of the public surface. _tools_removed: list[str] | None = None @@ -8214,6 +8456,7 @@ def from_dict(obj: Any) -> "PromptCacheBreakData": _tools_added = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("toolsAdded")) _tools_added_raw = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("toolsAddedRaw")) _tools_redefined = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("toolsRedefined")) + _tools_redefined_parts = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("toolsRedefinedParts")) _tools_redefined_raw = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("toolsRedefinedRaw")) _tools_removed = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("toolsRemoved")) _tools_removed_raw = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("toolsRemovedRaw")) @@ -8238,6 +8481,7 @@ def from_dict(obj: Any) -> "PromptCacheBreakData": _tools_added=_tools_added, _tools_added_raw=_tools_added_raw, _tools_redefined=_tools_redefined, + _tools_redefined_parts=_tools_redefined_parts, _tools_redefined_raw=_tools_redefined_raw, _tools_removed=_tools_removed, _tools_removed_raw=_tools_removed_raw, @@ -8278,6 +8522,8 @@ def to_dict(self) -> dict: result["toolsAddedRaw"] = from_union([from_none, lambda x: from_list(from_str, x)], self._tools_added_raw) if self._tools_redefined is not None: result["toolsRedefined"] = from_union([from_none, lambda x: from_list(from_str, x)], self._tools_redefined) + if self._tools_redefined_parts is not None: + result["toolsRedefinedParts"] = from_union([from_none, lambda x: from_list(from_str, x)], self._tools_redefined_parts) if self._tools_redefined_raw is not None: result["toolsRedefinedRaw"] = from_union([from_none, lambda x: from_list(from_str, x)], self._tools_redefined_raw) if self._tools_removed is not None: @@ -9571,6 +9817,7 @@ class SessionModelChangeData: previous_reasoning_summary: ReasoningSummary | None = None previous_verbosity: Verbosity | None = None reasoning_effort: str | None = None + reasoning_effort_model: str | None = None reasoning_summary: ReasoningSummary | None = None source: ModelChangeSource | None = None verbosity: Verbosity | None = None @@ -9588,6 +9835,7 @@ def from_dict(obj: Any) -> "SessionModelChangeData": previous_reasoning_summary = from_union([from_none, lambda x: parse_enum(ReasoningSummary, x)], obj.get("previousReasoningSummary")) previous_verbosity = from_union([from_none, lambda x: parse_enum(Verbosity, x)], obj.get("previousVerbosity")) reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) + reasoning_effort_model = from_union([from_none, from_str], obj.get("reasoningEffortModel")) reasoning_summary = from_union([from_none, lambda x: parse_enum(ReasoningSummary, x)], obj.get("reasoningSummary")) source = from_union([from_none, lambda x: parse_enum(ModelChangeSource, x)], obj.get("source")) verbosity = from_union([from_none, lambda x: parse_enum(Verbosity, x)], obj.get("verbosity")) @@ -9602,6 +9850,7 @@ def from_dict(obj: Any) -> "SessionModelChangeData": previous_reasoning_summary=previous_reasoning_summary, previous_verbosity=previous_verbosity, reasoning_effort=reasoning_effort, + reasoning_effort_model=reasoning_effort_model, reasoning_summary=reasoning_summary, source=source, verbosity=verbosity, @@ -9628,6 +9877,8 @@ def to_dict(self) -> dict: result["previousVerbosity"] = from_union([from_none, lambda x: to_enum(Verbosity, x)], self.previous_verbosity) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_none, from_str], self.reasoning_effort) + if self.reasoning_effort_model is not None: + result["reasoningEffortModel"] = from_union([from_none, from_str], self.reasoning_effort_model) if self.reasoning_summary is not None: result["reasoningSummary"] = from_union([from_none, lambda x: to_enum(ReasoningSummary, x)], self.reasoning_summary) if self.source is not None: @@ -9749,6 +10000,7 @@ class SessionResumeData: continue_pending_work: bool | None = None events_file_size_bytes: int | None = None reasoning_effort: str | None = None + reasoning_effort_model: str | None = None reasoning_summary: ReasoningSummary | None = None remote_steerable: bool | None = None selected_model: str | None = None @@ -9768,6 +10020,7 @@ def from_dict(obj: Any) -> "SessionResumeData": continue_pending_work = from_union([from_none, from_bool], obj.get("continuePendingWork")) events_file_size_bytes = from_union([from_none, from_int], obj.get("eventsFileSizeBytes")) reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) + reasoning_effort_model = from_union([from_none, from_str], obj.get("reasoningEffortModel")) reasoning_summary = from_union([from_none, lambda x: parse_enum(ReasoningSummary, x)], obj.get("reasoningSummary")) remote_steerable = from_union([from_none, from_bool], obj.get("remoteSteerable")) selected_model = from_union([from_none, from_str], obj.get("selectedModel")) @@ -9784,6 +10037,7 @@ def from_dict(obj: Any) -> "SessionResumeData": continue_pending_work=continue_pending_work, events_file_size_bytes=events_file_size_bytes, reasoning_effort=reasoning_effort, + reasoning_effort_model=reasoning_effort_model, reasoning_summary=reasoning_summary, remote_steerable=remote_steerable, selected_model=selected_model, @@ -9810,6 +10064,8 @@ def to_dict(self) -> dict: result["eventsFileSizeBytes"] = from_union([from_none, to_int], self.events_file_size_bytes) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_none, from_str], self.reasoning_effort) + if self.reasoning_effort_model is not None: + result["reasoningEffortModel"] = from_union([from_none, from_str], self.reasoning_effort_model) if self.reasoning_summary is not None: result["reasoningSummary"] = from_union([from_none, lambda x: to_enum(ReasoningSummary, x)], self.reasoning_summary) if self.remote_steerable is not None: @@ -10103,6 +10359,7 @@ class SessionStartData: detached_from_spawning_parent_session_id: str | None = None github_mcp_tool_config: GitHubMcpToolConfig | None = None reasoning_effort: str | None = None + reasoning_effort_model: str | None = None reasoning_summary: ReasoningSummary | None = None remote_steerable: bool | None = None selected_model: str | None = None @@ -10124,6 +10381,7 @@ def from_dict(obj: Any) -> "SessionStartData": detached_from_spawning_parent_session_id = from_union([from_none, from_str], obj.get("detachedFromSpawningParentSessionId")) github_mcp_tool_config = from_union([from_none, GitHubMcpToolConfig.from_dict], obj.get("githubMcpToolConfig")) reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) + reasoning_effort_model = from_union([from_none, from_str], obj.get("reasoningEffortModel")) reasoning_summary = from_union([from_none, lambda x: parse_enum(ReasoningSummary, x)], obj.get("reasoningSummary")) remote_steerable = from_union([from_none, from_bool], obj.get("remoteSteerable")) selected_model = from_union([from_none, from_str], obj.get("selectedModel")) @@ -10142,6 +10400,7 @@ def from_dict(obj: Any) -> "SessionStartData": detached_from_spawning_parent_session_id=detached_from_spawning_parent_session_id, github_mcp_tool_config=github_mcp_tool_config, reasoning_effort=reasoning_effort, + reasoning_effort_model=reasoning_effort_model, reasoning_summary=reasoning_summary, remote_steerable=remote_steerable, selected_model=selected_model, @@ -10170,6 +10429,8 @@ def to_dict(self) -> dict: result["githubMcpToolConfig"] = from_union([from_none, lambda x: to_class(GitHubMcpToolConfig, x)], self.github_mcp_tool_config) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_none, from_str], self.reasoning_effort) + if self.reasoning_effort_model is not None: + result["reasoningEffortModel"] = from_union([from_none, from_str], self.reasoning_effort_model) if self.reasoning_summary is not None: result["reasoningSummary"] = from_union([from_none, lambda x: to_enum(ReasoningSummary, x)], self.reasoning_summary) if self.remote_steerable is not None: @@ -11916,6 +12177,8 @@ class ToolExecutionCompleteData: tool_call_id: str error: ToolExecutionCompleteError | None = None # Experimental: this field is part of an experimental API and may change or be removed. + file_edits: list[ToolExecutionCompleteFileEdit] | None = None + # Experimental: this field is part of an experimental API and may change or be removed. fusion: FusionAttribution | None = None interaction_id: str | None = None is_user_requested: bool | None = None @@ -11939,6 +12202,7 @@ def from_dict(obj: Any) -> "ToolExecutionCompleteData": success = from_bool(obj.get("success")) tool_call_id = from_str(obj.get("toolCallId")) error = from_union([from_none, ToolExecutionCompleteError.from_dict], obj.get("error")) + file_edits = from_union([from_none, lambda x: from_list(ToolExecutionCompleteFileEdit.from_dict, x)], obj.get("fileEdits")) fusion = from_union([from_none, FusionAttribution.from_dict], obj.get("fusion")) interaction_id = from_union([from_none, from_str], obj.get("interactionId")) is_user_requested = from_union([from_none, from_bool], obj.get("isUserRequested")) @@ -11956,6 +12220,7 @@ def from_dict(obj: Any) -> "ToolExecutionCompleteData": success=success, tool_call_id=tool_call_id, error=error, + file_edits=file_edits, fusion=fusion, interaction_id=interaction_id, is_user_requested=is_user_requested, @@ -11977,6 +12242,8 @@ def to_dict(self) -> dict: result["toolCallId"] = from_str(self.tool_call_id) if self.error is not None: result["error"] = from_union([from_none, lambda x: to_class(ToolExecutionCompleteError, x)], self.error) + if self.file_edits is not None: + result["fileEdits"] = from_union([from_none, lambda x: from_list(lambda x: to_class(ToolExecutionCompleteFileEdit, x), x)], self.file_edits) if self.fusion is not None: result["fusion"] = from_union([from_none, lambda x: to_class(FusionAttribution, x)], self.fusion) if self.interaction_id is not None: @@ -12377,49 +12644,32 @@ def to_dict(self) -> dict: return {} -@dataclass -class ToolExecutionPartialResultData: - "Streaming tool execution output for incremental result display" - partial_output: str - tool_call_id: str - - @staticmethod - def from_dict(obj: Any) -> "ToolExecutionPartialResultData": - assert isinstance(obj, dict) - partial_output = from_str(obj.get("partialOutput")) - tool_call_id = from_str(obj.get("toolCallId")) - return ToolExecutionPartialResultData( - partial_output=partial_output, - tool_call_id=tool_call_id, - ) - - def to_dict(self) -> dict: - result: dict = {} - result["partialOutput"] = from_str(self.partial_output) - result["toolCallId"] = from_str(self.tool_call_id) - return result - - @dataclass class ToolExecutionProgressData: "Tool execution progress notification with status message" progress_message: str tool_call_id: str + # Experimental: this field is part of an experimental API and may change or be removed. + structured_content: Any = None @staticmethod def from_dict(obj: Any) -> "ToolExecutionProgressData": assert isinstance(obj, dict) progress_message = from_str(obj.get("progressMessage")) tool_call_id = from_str(obj.get("toolCallId")) + structured_content = obj.get("structuredContent") return ToolExecutionProgressData( progress_message=progress_message, tool_call_id=tool_call_id, + structured_content=structured_content, ) def to_dict(self) -> dict: result: dict = {} result["progressMessage"] = from_str(self.progress_message) result["toolCallId"] = from_str(self.tool_call_id) + if self.structured_content is not None: + result["structuredContent"] = self.structured_content return result @@ -12649,6 +12899,38 @@ def to_dict(self) -> dict: return result +@dataclass +class ToolShellOutputData: + "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." + sequence: int + text: str + tool_call_id: str + stream: ToolShellOutputStream | None = None + + @staticmethod + def from_dict(obj: Any) -> "ToolShellOutputData": + assert isinstance(obj, dict) + sequence = from_int(obj.get("sequence")) + text = from_str(obj.get("text")) + tool_call_id = from_str(obj.get("toolCallId")) + stream = from_union([from_none, lambda x: parse_enum(ToolShellOutputStream, x)], obj.get("stream")) + return ToolShellOutputData( + sequence=sequence, + text=text, + tool_call_id=tool_call_id, + stream=stream, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["sequence"] = to_int(self.sequence) + result["text"] = from_str(self.text) + result["toolCallId"] = from_str(self.tool_call_id) + if self.stream is not None: + result["stream"] = from_union([from_none, lambda x: to_enum(ToolShellOutputStream, x)], self.stream) + return result + + @dataclass class ToolUserRequestedData: "User-initiated tool invocation request with tool name and arguments" @@ -13477,6 +13759,26 @@ class PermissionRecommendation(Enum): APPROVE = "approve" +# Experimental: this enum is part of an experimental API and may change or be removed. +class ToolExecutionCompleteFileEditKind(Enum): + "Kind of file mutation committed by a built-in editing tool." + # A file was created. + CREATE = "create" + # A file was written by an edit operation. + EDIT = "edit" + # A file was deleted. + DELETE = "delete" + + @classmethod + def _missing_(cls, value: object) -> "ToolExecutionCompleteFileEditKind | 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) + + # Experimental: this enum is part of an experimental API and may change or be removed. class UIEphemeralQueryPhase(Enum): "Lifecycle phase for a Rust-owned ephemeral query stream." @@ -13765,6 +14067,23 @@ class HandoffSourceType(Enum): LOCAL = "local" +class HumanResponseActor(Enum): + "Controlled provenance for a typed runtime response. Only `human_response`, minted by a trusted direct-interaction ingress, is human authorization evidence." + # A built-in trusted client submitted the response after direct human interaction. + HUMAN_RESPONSE = "human_response" + # A host or SDK automation submitted the response without direct human interaction. + HOST_AUTOMATION = "host_automation" + # The response came through a legacy or otherwise unattributed ingress. + UNKNOWN = "unknown" + + +class HumanResponseRecordedResponseResponseKind(Enum): + "Exact runtime-owned question or reviewed plan paired with the typed response that settled it. discriminator" + ASK_USER = "ask_user" + USER_INPUT = "user_input" + EXIT_PLAN_MODE = "exit_plan_mode" + + class IndexedSearchDisabledReason(Enum): "Configuration, policy, or workspace condition that disabled indexed search." # Indexed search was explicitly disabled by the environment. @@ -13941,7 +14260,7 @@ class McpServerStatus(Enum): PENDING = "pending" # The server is configured but disabled. 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. STOPPED = "stopped" # The server is not configured for this session. NOT_CONFIGURED = "not_configured" @@ -13993,6 +14312,26 @@ class ModelCallFailureTransport(Enum): WEBSOCKET = "websocket" +class ModelCallFinalResult(Enum): + "Final bounded result of one logical model operation after its internal retry loop settles" + # The final attempt succeeded. + SUCCESS = "success" + # The final attempt failed with HTTP 400. + HTTP_400 = "http_400" + # The final attempt failed with HTTP 413. + HTTP_413 = "http_413" + # The final attempt failed with HTTP 429. + HTTP_429 = "http_429" + # The final attempt failed with another HTTP 4xx status. + HTTP_4XX = "http_4xx" + # The final attempt failed with an HTTP 5xx status. + HTTP_5XX = "http_5xx" + # The final attempt failed in the request transport. + TRANSPORT_ERROR = "transport_error" + # The final attempt failed without another bounded classification. + OTHER_ERROR = "other_error" + + class ModelCallFinishedOutcome(Enum): "Final outcome of one logical model dispatch after response acceptance processing" # The provider response was accepted for continued agent processing. @@ -14705,6 +15044,16 @@ class ToolExecutionStartToolDescriptionMetaUIVisibility(Enum): APP = "app" +class ToolShellOutputStream(Enum): + "Shell output source. Terminal output has no separate stdout/stderr attribution." + # Output from the shell command's standard output stream. This is the default when stream is omitted. + STDOUT = "stdout" + # Output from the shell command's standard error stream. + STDERR = "stderr" + # Inherently merged output that cannot be attributed separately to stdout or stderr. + TERMINAL = "terminal" + + class UserMessageAgentMode(Enum): "The agent mode that was active when this message was sent" # The agent is responding interactively to the user. @@ -14775,7 +15124,7 @@ class WorkspaceFileChangedOperation(Enum): UPDATE = "update" -SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionScheduleCreatedData | SessionScheduleCancelledData | SessionScheduleRearmedData | SessionAutopilotObjectiveChangedData | SessionInfoData | SessionIndexedSearchData | SessionWarningData | SessionModelChangeData | SessionModelDeselectedData | SessionAutoTierRecommendationData | SessionAutoTierSwitchFailedData | SessionModeChangedData | SessionModeNoticeDeliveredData | SessionSessionLimitsChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionUsageCheckpointData | SessionContextChangedData | SessionUsageInfoData | SessionContextClearedData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | SessionCompletionReceiptData | SessionFusionRouteStartedData | SessionFusionRouteFailedData | SessionFusionResolvedData | SessionFusionCompletedData | SessionPermissionRecoveryData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantTurnRetryData | AgentInterruptedData | AssistantIntentData | AssistantFusionPhaseStartedData | AssistantFusionPhaseActivityData | AssistantFusionPhaseCompletedData | AssistantFusionPhaseFailedData | AssistantServerToolProgressData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantToolCallDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | PromptCacheBreakData | ModelCallFailureData | ModelCallFinishedData | ModelCallStartData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | ToolSearchActivatedData | SkillInvokedData | SkillInvokedRefData | SkillContextDeliveredData | SkillContextDeliveredRefData | SandboxDecisionData | SubagentStartedData | SubagentConfiguredData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | PermissionCarriedForwardData | PermissionMessageAuthorizationData | PermissionMessageAuthorizationReadData | PermissionMessageAuthorizationDegradedData | PermissionAssentDetectedData | PermissionContextualAuthorizationData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | UiEphemeralQueryData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | SessionAutoModeResolvedData | SessionManagedSettingsResolvedData | SessionManagedSettingsEnforcedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | WorkflowRunUpdatedData | WorkflowRunStartedData | WorkflowRunSettledData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | SessionMcpServerRemovedData | SessionMcpServerNeedsReconnectData | McpToolsListChangedData | McpResourcesListChangedData | McpPromptsListChangedData | SessionExtensionsLoadedData | SessionCanvasOpenedData | SessionCanvasRegistryChangedData | SessionCanvasClosedData | SessionCanvasUnavailableData | SessionCanvasRecordedData | SessionCanvasRemovedData | SessionExtensionsAttachmentsPushedData | McpAppToolCallCompleteData | RawSessionEventData | Data +SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionScheduleCreatedData | SessionScheduleCancelledData | SessionScheduleRearmedData | SessionAutopilotObjectiveChangedData | SessionInfoData | SessionIndexedSearchData | SessionWarningData | SessionModelChangeData | SessionModelDeselectedData | SessionAutoTierRecommendationData | SessionAutoTierSwitchFailedData | SessionModeChangedData | SessionModeNoticeDeliveredData | SessionSessionLimitsChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionUsageCheckpointData | SessionContextChangedData | SessionUsageInfoData | SessionContextClearedData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | SessionCompletionReceiptData | SessionFusionRouteStartedData | SessionFusionRouteFailedData | SessionFusionResolvedData | SessionFusionCompletedData | SessionPermissionRecoveryData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantTurnRetryData | AgentInterruptedData | AssistantIntentData | AssistantFusionPhaseStartedData | AssistantFusionPhaseActivityData | AssistantFusionPhaseCompletedData | AssistantFusionPhaseFailedData | AssistantServerToolProgressData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantToolCallDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | PromptCacheBreakData | ModelCallFailureData | ModelCallFinalResultData | ModelCallFinishedData | ModelCallStartData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolShellOutputData | ToolExecutionProgressData | ToolExecutionCompleteData | ToolSearchActivatedData | SkillInvokedData | SkillInvokedRefData | SkillContextDeliveredData | SkillContextDeliveredRefData | SandboxDecisionData | SubagentStartedData | SubagentConfiguredData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | PermissionCarriedForwardData | PermissionMessageAuthorizationData | PermissionMessageAuthorizationReadData | PermissionMessageAuthorizationDegradedData | PermissionAssentDetectedData | PermissionContextualAuthorizationData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | UiEphemeralQueryData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | SessionAutoModeResolvedData | SessionManagedSettingsResolvedData | SessionManagedSettingsEnforcedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | HumanResponseRecordedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | WorkflowRunUpdatedData | WorkflowRunStartedData | WorkflowRunSettledData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | SessionMcpServerRemovedData | SessionMcpServerNeedsReconnectData | McpToolsListChangedData | McpResourcesListChangedData | McpPromptsListChangedData | SessionExtensionsLoadedData | SessionCanvasOpenedData | SessionCanvasRegistryChangedData | SessionCanvasClosedData | SessionCanvasUnavailableData | SessionCanvasRecordedData | SessionCanvasRemovedData | SessionExtensionsAttachmentsPushedData | McpAppToolCallCompleteData | RawSessionEventData | Data @dataclass @@ -14865,12 +15214,14 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.ASSISTANT_USAGE: data = AssistantUsageData.from_dict(data_obj) case SessionEventType.PROMPT_CACHE_BREAK: data = PromptCacheBreakData.from_dict(data_obj) case SessionEventType.MODEL_CALL_FAILURE: data = ModelCallFailureData.from_dict(data_obj) + case SessionEventType.MODEL_CALL_FINAL_RESULT: data = ModelCallFinalResultData.from_dict(data_obj) case SessionEventType.MODEL_CALL_FINISHED: data = ModelCallFinishedData.from_dict(data_obj) case SessionEventType.MODEL_CALL_START: data = ModelCallStartData.from_dict(data_obj) case SessionEventType.ABORT: data = AbortData.from_dict(data_obj) case SessionEventType.TOOL_USER_REQUESTED: data = ToolUserRequestedData.from_dict(data_obj) case SessionEventType.TOOL_EXECUTION_START: data = ToolExecutionStartData.from_dict(data_obj) case SessionEventType.TOOL_EXECUTION_PARTIAL_RESULT: data = ToolExecutionPartialResultData.from_dict(data_obj) + case SessionEventType.TOOL_SHELL_OUTPUT: data = ToolShellOutputData.from_dict(data_obj) case SessionEventType.TOOL_EXECUTION_PROGRESS: data = ToolExecutionProgressData.from_dict(data_obj) case SessionEventType.TOOL_EXECUTION_COMPLETE: data = ToolExecutionCompleteData.from_dict(data_obj) case SessionEventType.TOOL_SEARCH_ACTIVATED: data = ToolSearchActivatedData.from_dict(data_obj) @@ -14927,6 +15278,7 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.CAPABILITIES_CHANGED: data = CapabilitiesChangedData.from_dict(data_obj) case SessionEventType.EXIT_PLAN_MODE_REQUESTED: data = ExitPlanModeRequestedData.from_dict(data_obj) case SessionEventType.EXIT_PLAN_MODE_COMPLETED: data = ExitPlanModeCompletedData.from_dict(data_obj) + case SessionEventType.HUMAN_RESPONSE_RECORDED: data = HumanResponseRecordedData.from_dict(data_obj) case SessionEventType.SESSION_TOOLS_UPDATED: data = SessionToolsUpdatedData.from_dict(data_obj) case SessionEventType.SESSION_BACKGROUND_TASKS_CHANGED: data = SessionBackgroundTasksChangedData.from_dict(data_obj) case SessionEventType.WORKFLOW_RUN_UPDATED: data = WorkflowRunUpdatedData.from_dict(data_obj) @@ -15120,6 +15472,10 @@ def session_event_to_dict(x: SessionEvent) -> Any: "HookEndError", "HookProgressData", "HookStartData", + "HumanResponseActor", + "HumanResponseRecordedData", + "HumanResponseRecordedResponse", + "HumanResponseRecordedResponseResponseKind", "IndexedSearchDisabledReason", "IndexedSearchErrorType", "IndexedSearchIncrementalPhase", @@ -15157,6 +15513,8 @@ def session_event_to_dict(x: SessionEvent) -> Any: "ModelCallFailureRequestFingerprint", "ModelCallFailureSource", "ModelCallFailureTransport", + "ModelCallFinalResult", + "ModelCallFinalResultData", "ModelCallFinishedData", "ModelCallFinishedOutcome", "ModelCallStartData", @@ -15397,6 +15755,8 @@ def session_event_to_dict(x: SessionEvent) -> Any: "ToolExecutionCompleteContentText", "ToolExecutionCompleteData", "ToolExecutionCompleteError", + "ToolExecutionCompleteFileEdit", + "ToolExecutionCompleteFileEditKind", "ToolExecutionCompleteResult", "ToolExecutionCompleteShellExecution", "ToolExecutionCompleteToolDescription", @@ -15421,6 +15781,8 @@ def session_event_to_dict(x: SessionEvent) -> Any: "ToolExecutionStartToolDescriptionMetaUI", "ToolExecutionStartToolDescriptionMetaUIVisibility", "ToolSearchActivatedData", + "ToolShellOutputData", + "ToolShellOutputStream", "ToolUserRequestedData", "UIEphemeralQueryPhase", "UiEphemeralQueryData", diff --git a/python/copilot/host.py b/python/copilot/host.py index 29bfca3be9..1a64122c02 100644 --- a/python/copilot/host.py +++ b/python/copilot/host.py @@ -15,7 +15,13 @@ from uuid import uuid4 from weakref import WeakKeyDictionary -from .generated.rpc import HostExitedNotification, HostPublishSessionResult, HostStartResult +from .generated.rpc import ( + HostExitedNotification, + HostGitHubEnvironmentOptions, + HostLocalServerOptions, + HostPublishSessionResult, + HostStartResult, +) if TYPE_CHECKING: from ._jsonrpc import JsonRpcClient @@ -59,9 +65,10 @@ class AhpHost: """ host_id: str - url: str + url: str | None token: str | None pid: int | None + environment_id: str | None _rpc: JsonRpcClient = field(repr=False, compare=False) async def dispose(self) -> None: @@ -84,17 +91,17 @@ async def __aexit__(self, *_: object) -> None: @dataclass(frozen=True) class AhpHostOptions: - """Local callbacks and listener settings for experimental AHP hosting. + """Local callbacks and transport settings for experimental AHP hosting. + Select ``local_server``, ``github_environment``, or both. An empty local + server selects loopback defaults. GitHub-only hosting has no local URL. Factories must return the exact requested session from the owning client. Release callbacks receive that same object once per handoff, including late results. The SDK never disconnects or destroys it on the application's behalf. """ - hostname: str | None = None - port: int | None = None - token: str | None = None - require_connection_token: bool | None = None + local_server: HostLocalServerOptions | None = None + github_environment: HostGitHubEnvironmentOptions | None = None create_session: Callable[[AhpSessionCreateRequest], Awaitable[CopilotSession]] | None = None resume_session: Callable[[AhpSessionResumeRequest], Awaitable[CopilotSession]] | None = None on_session_released: Callable[[CopilotSession], Awaitable[None] | None] | None = None @@ -198,14 +205,20 @@ def capture(self, session: CopilotSession, config: dict[str, Any]) -> None: entry.configs[session] = copy.deepcopy(config) async def start(self, rpc: JsonRpcClient, options: AhpHostOptions) -> AhpHost: + if options.local_server is None and options.github_environment is None: + raise ValueError("At least one of local_server or github_environment is required") host_id = str(uuid4()) self._hosts[host_id] = options params = { "hostId": host_id, - "hostname": options.hostname, - "port": options.port, - "token": options.token, - "requireConnectionToken": options.require_connection_token, + "localServer": ( + options.local_server.to_dict() if options.local_server is not None else None + ), + "githubEnvironment": ( + options.github_environment.to_dict() + if options.github_environment is not None + else None + ), "sessionFactory": True if options.create_session else None, "resumeFactory": True if options.resume_session else None, } @@ -216,7 +229,7 @@ async def start(self, rpc: JsonRpcClient, options: AhpHostOptions) -> AhpHost: ) try: info = HostStartResult.from_dict(await asyncio.shield(startup)) - return AhpHost(info.host_id, info.url, info.token, info.pid, rpc) + return AhpHost(info.host_id, info.url, info.token, info.pid, info.environment_id, rpc) except asyncio.CancelledError: # Settle startup before disposing: the runtime may not have registered # the host yet, and canceling the local RPC does not cancel that work. diff --git a/python/copilot/session.py b/python/copilot/session.py index 9610c8ea21..91174ce370 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -45,6 +45,7 @@ LogRequest, MCPOauthHandlePendingRequest, MCPOauthPendingRequestResponse, + MCPServerConfigDeferTools, ModelSwitchAutoTierResult, ModelSwitchToRequest, PermissionDecision, @@ -52,10 +53,12 @@ PermissionDecisionContext, PermissionDecisionRequest, PermissionDecisionUserNotAvailable, + ProtocolExternalToolDefinition, ProviderTokenAcquireRequest, ProviderTokenAcquireResult, SessionLogLevel, SessionRpc, + ToolsSetRequest, UIElicitationRequest, UIElicitationResponse, UIElicitationResponseAction, @@ -215,8 +218,38 @@ def _auto_tier_to_wire(auto_tier: AutoTier | _RpcAutoTier | None) -> str | None: return auto_tier +def _tool_to_wire_definition(tool: Tool) -> ProtocolExternalToolDefinition: + defer = MCPServerConfigDeferTools(tool.defer) if tool.defer is not None else None + return ProtocolExternalToolDefinition( + name=tool.name, + description=tool.description or "", + parameters=tool.parameters or None, + overrides_built_in_tool=True if tool.overrides_built_in_tool else None, + skip_permission=True if tool.skip_permission else None, + defer=defer, + metadata=tool.metadata, + is_terminal=True if tool.is_terminal else None, + ) + + +def _tools_to_wire_definitions(tools: list[Tool] | None) -> list[ProtocolExternalToolDefinition]: + return [_tool_to_wire_definition(tool) for tool in tools or []] + + +def _tool_handlers_for(tools: list[Tool] | None) -> dict[str, ToolHandler]: + return { + tool.name: tool.handler for tool in tools or [] if tool.name and tool.handler is not None + } + + +def _consume_task_exception(task: asyncio.Task[Any]) -> None: + if not task.cancelled(): + task.exception() + + class SessionFsCapabilities(TypedDict, total=False): sqlite: bool + binary: bool class SessionFsConfig(TypedDict): @@ -384,12 +417,13 @@ class SystemMessageReplaceConfig(TypedDict): "tool_instructions": "Per-tool usage instructions", "custom_instructions": "Repository and organization custom instructions", "runtime_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." ), "last_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" ), } @@ -1194,6 +1228,67 @@ class AgentStopHookOutput(TypedDict, total=False): ] +class SubagentStartHookInput(TypedDict): + """Input when a subagent starts; session metadata belongs to its parent.""" + + sessionId: str + timestamp: datetime + workingDirectory: str + transcriptPath: str + agentName: str + agentDisplayName: NotRequired[str] + agentDescription: NotRequired[str] + + +class SubagentStartHookOutput(TypedDict, total=False): + """Context prepended to the subagent's initial prompt.""" + + additionalContext: str + + +SubagentStartHandler = Callable[ + [SubagentStartHookInput, dict[str, str]], + SubagentStartHookOutput | None | Awaitable[SubagentStartHookOutput | None], +] + + +class SubagentStopHookInput(TypedDict): + """Input after a subagent turn completes; session metadata belongs to its parent.""" + + sessionId: str + timestamp: datetime + workingDirectory: str + transcriptPath: str + agentName: str + agentType: str + stopReason: Literal["end_turn"] + response: str + agentId: NotRequired[str] + agentDisplayName: NotRequired[str] + agentDescription: NotRequired[str] + + +class _SubagentStopBlockHookOutput(TypedDict): + decision: Literal["block"] + reason: str + modifiedResponse: NotRequired[str] + + +class _SubagentStopAllowHookOutput(TypedDict, total=False): + decision: Literal["allow"] + modifiedResponse: str + + +SubagentStopHookOutput = _SubagentStopBlockHookOutput | _SubagentStopAllowHookOutput +"""Block with a nonempty reason, or allow with an optional response rewrite.""" + + +SubagentStopHandler = Callable[ + [SubagentStopHookInput, dict[str, str]], + SubagentStopHookOutput | None | Awaitable[SubagentStopHookOutput | None], +] + + class SessionHooks(TypedDict, total=False): """Configuration for session hooks""" @@ -1207,6 +1302,8 @@ class SessionHooks(TypedDict, total=False): on_session_end: SessionEndHandler on_error_occurred: ErrorOccurredHandler on_agent_stop: AgentStopHandler + on_subagent_start: SubagentStartHandler + on_subagent_stop: SubagentStopHandler # ============================================================================ @@ -1410,6 +1507,18 @@ class ProviderTokenArgs(TypedDict): BearerTokenProvider = Callable[[ProviderTokenArgs], str | Awaitable[str]] +ProviderConfigModelProvider = Literal[ + "openai", + "anthropic", + "azure_openai", + "ollama", + "lm_studio", + "foundry_local", + "llama_cpp", +] +"""Product serving a configured provider's model.""" + + class ProviderConfig(TypedDict, total=False): """Configuration for a custom API provider""" @@ -1420,6 +1529,9 @@ class ProviderConfig(TypedDict, total=False): # connection instead of HTTP. Applies to OpenAI-compatible providers using # wire_api "responses". transport: Literal["http", "websockets"] + # Product serving the model, such as "ollama" or "lm_studio", reported in + # telemetry as model_provider. Only affects telemetry. + model_provider: ProviderConfigModelProvider base_url: str api_key: str # Bearer token for authentication. Sets the Authorization header directly. @@ -1473,6 +1585,9 @@ class NamedProviderConfig(TypedDict, total=False): name: str type: Literal["openai", "azure", "anthropic"] wire_api: Literal["completions", "responses"] + # Product serving this provider's models, such as "ollama" or "lm_studio", + # reported in telemetry as model_provider. Only affects telemetry. + model_provider: ProviderConfigModelProvider base_url: str api_key: str # Bearer token for authentication. Sets the Authorization header directly. @@ -1590,6 +1705,15 @@ async def get_token(self, params: ProviderTokenAcquireRequest) -> ProviderTokenA return ProviderTokenAcquireResult(token=cast(str, result)) +@dataclass(frozen=True) +class TranscriptRecoveryReport: + """Repair details returned by ``session.resume`` when transcript recovery ran.""" + + planned_backup_path: str + invalid_line_numbers: list[int] + session_start_moved: bool + + class CopilotSession: """ Represents a single conversation session with the Copilot CLI. @@ -1642,6 +1766,7 @@ def __init__( creating or resuming the session. """ self.session_id = session_id + self.transcript_recovery: TranscriptRecoveryReport | None = None self._managed_settings_enabled = managed_settings_enabled self._client = client self._workspace_path = os.fsdecode(workspace_path) if workspace_path is not None else None @@ -1649,6 +1774,7 @@ def __init__( self._event_handlers_lock = threading.Lock() self._tool_handlers: dict[str, ToolHandler] = {} self._tool_handlers_lock = threading.Lock() + self._set_tools_lock = asyncio.Lock() self._pending_external_tools: dict[str, asyncio.Task[None]] = {} self._permission_handler: _PermissionHandlerFn | None = None self._permission_handler_lock = threading.Lock() @@ -1847,6 +1973,54 @@ async def send( ) return message_id + async def set_tools(self, tools: list[Tool]) -> None: + """ + **Experimental.** Replace this client's live external tools for the session. + + This is a complete replacement of the tools supplied by this client + connection only; built-in tools, MCP/plugin tools, and tools supplied by + other connections are unaffected. Pass the same tool definitions accepted + by ``CopilotClient.create_session`` and ``CopilotClient.resume_session``. + An empty list removes all tools supplied by this client. + + The local handlers switch only after the runtime accepts the replacement. + Running tool calls finish on the handlers they already captured. If the + runtime rejects the replacement, the existing handlers are left unchanged + and the error is raised. Concurrent ``set_tools`` calls on the same + session are applied in order. Cancelling the call 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. + + 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. + """ + definitions = _tools_to_wire_definitions(tools) + handlers = _tool_handlers_for(tools) + # Cancelling while an earlier call holds the lock sends nothing. Once + # this call holds it, the request runs to completion in its own task, + # so an accepted replacement still installs its handlers. + await self._set_tools_lock.acquire() + task = asyncio.create_task(self._replace_tools(definitions, handlers)) + task.add_done_callback(_consume_task_exception) + await asyncio.shield(task) + + async def _replace_tools( + self, + definitions: list[ProtocolExternalToolDefinition], + handlers: dict[str, ToolHandler], + ) -> None: + try: + await self.rpc.tools.set(ToolsSetRequest(tools=definitions)) + with self._tool_handlers_lock: + self._tool_handlers = handlers + finally: + self._set_tools_lock.release() + async def send_and_wait( self, prompt: str, @@ -2888,13 +3062,7 @@ def _register_tools(self, tools: list[Tool] | None) -> None: all registered tools. """ with self._tool_handlers_lock: - self._tool_handlers.clear() - if not tools: - return - for tool in tools: - if not tool.name or not tool.handler: - continue - self._tool_handlers[tool.name] = tool.handler + self._tool_handlers = _tool_handlers_for(tools) def _get_tool_handler(self, name: str) -> ToolHandler | None: """ @@ -3184,6 +3352,8 @@ async def _handle_hooks_invoke(self, hook_type: str, input_data: Any) -> Any: "sessionEnd": hooks.get("on_session_end"), "errorOccurred": hooks.get("on_error_occurred"), "agentStop": hooks.get("on_agent_stop"), + "subagentStart": hooks.get("on_subagent_start"), + "subagentStop": hooks.get("on_subagent_stop"), } handler = handler_map.get(hook_type) diff --git a/python/copilot/session_fs_provider.py b/python/copilot/session_fs_provider.py index c9e90a6442..605d6fe85a 100644 --- a/python/copilot/session_fs_provider.py +++ b/python/copilot/session_fs_provider.py @@ -17,11 +17,12 @@ from __future__ import annotations import abc +import base64 import errno from collections.abc import Sequence from dataclasses import dataclass from datetime import UTC, datetime -from typing import Any +from typing import Any, Protocol, runtime_checkable from .generated.rpc import ( SessionFSError, @@ -31,6 +32,7 @@ SessionFSReaddirResult, SessionFSReaddirWithTypesEntry, SessionFSReaddirWithTypesResult, + SessionFSReadFileBytesResult, SessionFSReadFileResult, SessionFSSqliteExistsResult, SessionFSSqliteQueryType, @@ -48,6 +50,9 @@ SessionFSSqliteTransactionResult as _GeneratedSqliteTransactionResult, ) +_MAX_BINARY_BYTES = (64 * 1024 * 1024 - 1024) // 4 * 3 +_MAX_BINARY_CONTENT_LENGTH = ((_MAX_BINARY_BYTES + 2) // 3) * 4 + @dataclass class SessionFsFileInfo: @@ -60,6 +65,10 @@ class SessionFsFileInfo: birthtime: datetime +class SessionFsWriteFailure(Exception): + """Raise from ``write_file`` only when the failed write changed its target.""" + + class SessionFsProvider(abc.ABC): """Abstract base class for session filesystem providers. @@ -75,7 +84,10 @@ async def read_file(self, path: str) -> str: @abc.abstractmethod async def write_file(self, path: str, content: str, mode: int | None = None) -> None: - """Write *content* to a file, creating parent directories if needed.""" + """Write *content* to a file, creating parent directories if needed. + + Raise :class:`SessionFsWriteFailure` if a failed write changed the target. + """ @abc.abstractmethod async def append_file(self, path: str, content: str, mode: int | None = None) -> None: @@ -110,6 +122,19 @@ async def rename(self, src: str, dest: str) -> None: """Rename / move a file or directory.""" +@runtime_checkable +class SessionFsBinaryProvider(Protocol): + """Optional interface for providers that support exact binary reads and writes.""" + + async def read_file_bytes(self, path: str) -> bytes: + """Read the exact bytes of a file. Raise if it does not exist.""" + ... + + async def write_file_bytes(self, path: str, content: bytes, mode: int | None = None) -> None: + """Write the exact bytes of a file, respecting the optional mode.""" + ... + + class SessionFsSqliteProvider(abc.ABC): """Optional ABC for providers that support SQLite operations. @@ -214,10 +239,42 @@ async def read_file(self, params: Any) -> SessionFSReadFileResult: err = _to_session_fs_error(exc) return SessionFSReadFileResult.from_dict({"content": "", "error": err.to_dict()}) + async def read_file_bytes(self, params: Any) -> SessionFSReadFileBytesResult: + try: + if not isinstance(self._p, SessionFsBinaryProvider): + raise NotImplementedError("Binary reads are not supported by this provider") + data = await self._p.read_file_bytes(params.path) + if len(data) > _MAX_BINARY_BYTES: + raise ValueError("sessionFs.readFileBytes content exceeds the binary read limit") + content = base64.b64encode(data).decode("ascii") + return SessionFSReadFileBytesResult.from_dict({"content": content}) + except Exception as exc: + err = _to_session_fs_error(exc) + return SessionFSReadFileBytesResult.from_dict({"content": "", "error": err.to_dict()}) + async def write_file(self, params: Any) -> SessionFSError | None: try: await self._p.write_file(params.path, params.content, getattr(params, "mode", None)) return None + except Exception as exc: + error = _to_session_fs_error(exc) + if isinstance(exc, SessionFsWriteFailure): + error.write_changed = True + return error + + async def write_file_bytes(self, params: Any) -> SessionFSError | None: + try: + if not isinstance(self._p, SessionFsBinaryProvider): + raise NotImplementedError("Binary writes are not supported by this provider") + if len(params.content) > _MAX_BINARY_CONTENT_LENGTH: + raise ValueError("sessionFs.writeFileBytes content exceeds the binary write limit") + data = base64.b64decode(params.content, validate=True) + if base64.b64encode(data).decode("ascii") != params.content: + raise ValueError("invalid sessionFs.writeFileBytes base64 content") + if len(data) > _MAX_BINARY_BYTES: + raise ValueError("sessionFs.writeFileBytes content exceeds the binary write limit") + await self._p.write_file_bytes(params.path, data, getattr(params, "mode", None)) + return None except Exception as exc: return _to_session_fs_error(exc) diff --git a/python/copilot/tools.py b/python/copilot/tools.py index ad0bcb41bd..efe76a47c3 100644 --- a/python/copilot/tools.py +++ b/python/copilot/tools.py @@ -1,3 +1,5 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + """ Tool definition utilities for the Copilot SDK. @@ -253,7 +255,7 @@ async def wrapped_handler(invocation: ToolInvocation) -> ToolResult: # Build args based on detected signature call_args = [] if takes_params: - args = invocation.arguments or {} + args = invocation.arguments if invocation.arguments is not None else {} if ptype is not None and _is_pydantic_model(ptype): try: call_args.append(ptype.model_validate(args)) diff --git a/python/e2e/test_agent_and_compact_rpc_e2e.py b/python/e2e/test_agent_and_compact_rpc_e2e.py deleted file mode 100644 index 300b2546aa..0000000000 --- a/python/e2e/test_agent_and_compact_rpc_e2e.py +++ /dev/null @@ -1,234 +0,0 @@ -"""E2E tests for Agent Selection and Session Compaction RPC APIs.""" - -import uuid - -import pytest - -from copilot import CopilotClient, RuntimeConnection -from copilot.rpc import AgentSelectRequest -from copilot.session import PermissionHandler - -from .testharness import CLI_PATH, E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -class TestAgentSelectionRpc: - @pytest.mark.asyncio - async def test_should_list_available_custom_agents(self): - """Test listing available custom agents via RPC.""" - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) - - try: - await client.start() - session = await client.create_session( - on_permission_request=PermissionHandler.approve_all, - custom_agents=[ - { - "name": "test-agent", - "display_name": "Test Agent", - "description": "A test agent", - "prompt": "You are a test agent.", - }, - { - "name": "another-agent", - "display_name": "Another Agent", - "description": "Another test agent", - "prompt": "You are another agent.", - }, - ], - ) - - result = await session.rpc.agent.list() - assert result.agents is not None - assert len(result.agents) == 2 - assert result.agents[0].name == "test-agent" - assert result.agents[0].display_name == "Test Agent" - assert result.agents[0].description == "A test agent" - assert result.agents[1].name == "another-agent" - - await session.disconnect() - await client.stop() - finally: - await client.force_stop() - - @pytest.mark.asyncio - async def test_should_return_null_when_no_agent_is_selected(self): - """Test getCurrent returns null when no agent is selected.""" - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) - - try: - await client.start() - session = await client.create_session( - on_permission_request=PermissionHandler.approve_all, - custom_agents=[ - { - "name": "test-agent", - "display_name": "Test Agent", - "description": "A test agent", - "prompt": "You are a test agent.", - } - ], - ) - - result = await session.rpc.agent.get_current() - assert result.agent is None - - await session.disconnect() - await client.stop() - finally: - await client.force_stop() - - @pytest.mark.asyncio - async def test_should_select_and_get_current_agent(self): - """Test selecting an agent and verifying getCurrent returns it.""" - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) - - try: - await client.start() - session = await client.create_session( - on_permission_request=PermissionHandler.approve_all, - custom_agents=[ - { - "name": "test-agent", - "display_name": "Test Agent", - "description": "A test agent", - "prompt": "You are a test agent.", - } - ], - ) - - # Select the agent - select_result = await session.rpc.agent.select(AgentSelectRequest(name="test-agent")) - assert select_result.agent is not None - assert select_result.agent.name == "test-agent" - assert select_result.agent.display_name == "Test Agent" - - # Verify getCurrent returns the selected agent - current_result = await session.rpc.agent.get_current() - assert current_result.agent is not None - assert current_result.agent.name == "test-agent" - - await session.disconnect() - await client.stop() - finally: - await client.force_stop() - - @pytest.mark.asyncio - async def test_should_deselect_current_agent(self): - """Test deselecting the current agent.""" - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) - - try: - await client.start() - session = await client.create_session( - on_permission_request=PermissionHandler.approve_all, - custom_agents=[ - { - "name": "test-agent", - "display_name": "Test Agent", - "description": "A test agent", - "prompt": "You are a test agent.", - } - ], - ) - - # Select then deselect - await session.rpc.agent.select(AgentSelectRequest(name="test-agent")) - await session.rpc.agent.deselect() - - # Verify no agent is selected - current_result = await session.rpc.agent.get_current() - assert current_result.agent is None - - await session.disconnect() - await client.stop() - finally: - await client.force_stop() - - @pytest.mark.asyncio - async def test_should_return_empty_list_when_no_custom_agents_configured(self): - """Test listing agents returns no custom agents when none configured.""" - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) - - try: - await client.start() - session = await client.create_session( - on_permission_request=PermissionHandler.approve_all - ) - - result = await session.rpc.agent.list() - # The CLI may return built-in/default agents even when no custom agents - # are configured. Verify no custom test agents appear in the list. - custom_names = {"test-agent", "another-agent"} - for agent in result.agents: - assert agent.name not in custom_names, ( - f"Expected no custom agents, but found {agent.name!r}" - ) - - await session.disconnect() - await client.stop() - finally: - await client.force_stop() - - @pytest.mark.asyncio - async def test_should_call_agent_reload(self): - """Test reloading agents via RPC.""" - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) - reload_agent = { - "name": f"reload-test-agent-{uuid.uuid4().hex}", - "display_name": "Reload Agent", - "description": "An agent used to validate reload", - "prompt": "You are a reload test agent.", - } - - try: - await client.start() - session = await client.create_session( - on_permission_request=PermissionHandler.approve_all, - custom_agents=[reload_agent], - ) - - before = await session.rpc.agent.list() - _assert_reload_agent(before.agents, reload_agent) - - result = await session.rpc.agent.reload() - assert result.agents is not None - current = await session.rpc.agent.list() - assert _agent_summaries(result.agents) == _agent_summaries(current.agents) - - await session.disconnect() - await client.stop() - finally: - await client.force_stop() - - -def _assert_reload_agent(agents, expected): - matches = [agent for agent in agents if agent.name == expected["name"]] - assert len(matches) == 1 - assert matches[0].display_name == expected["display_name"] - assert matches[0].description == expected["description"] - - -def _agent_summaries(agents): - return sorted((agent.name, agent.display_name) for agent in agents) - - -class TestSessionCompactionRpc: - @pytest.mark.asyncio - async def test_should_compact_session_history_after_messages(self, ctx: E2ETestContext): - """Test compacting session history via RPC.""" - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all - ) - - # Send a message to create some history - await session.send_and_wait("What is 2+2?") - - # Compact the session - result = await session.rpc.history.compact() - assert isinstance(result.success, bool) - assert isinstance(result.tokens_removed, (int, float)) - assert isinstance(result.messages_removed, (int, float)) - - await session.disconnect() diff --git a/python/e2e/test_builtin_tools_e2e.py b/python/e2e/test_builtin_tools_e2e.py deleted file mode 100644 index 64b5c12958..0000000000 --- a/python/e2e/test_builtin_tools_e2e.py +++ /dev/null @@ -1,166 +0,0 @@ -"""Smoke E2E coverage for Copilot CLI built-in tools.""" - -from __future__ import annotations - -import os -import re -from pathlib import Path - -import pytest - -from copilot.session import PermissionHandler - -from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - -# Built-in tool tests spawn a real CLI subprocess and execute actual shell / -# file tools. Under slow/concurrent CI (notably Windows) this agent loop can -# briefly exceed the 60s send_and_wait default, so give it extra headroom while -# still failing fast on a genuine hang. -SEND_TIMEOUT = 120.0 - - -class TestBuiltinTools: - async def test_should_capture_exit_code_in_output(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all - ) - try: - message = await session.send_and_wait( - "Run 'echo hello && echo world'. Tell me the exact output.", - timeout=SEND_TIMEOUT, - ) - content = message.data.content if message else "" - assert "hello" in content - assert "world" in content - finally: - await session.disconnect() - - @pytest.mark.skipif( - os.name == "nt", - reason="The stderr prompt uses bash syntax and is skipped by the TS suite on Windows.", - ) - async def test_should_capture_stderr_output(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all - ) - try: - message = await session.send_and_wait( - "Run 'echo error_msg >&2; sleep 0.5; echo ok' and tell me what stderr said. " - "Reply with just the stderr content.", - timeout=SEND_TIMEOUT, - ) - assert message is not None - assert "error_msg" in message.data.content - finally: - await session.disconnect() - - async def test_should_read_file_with_line_range(self, ctx: E2ETestContext): - Path(ctx.work_dir, "lines.txt").write_text( - "line1\nline2\nline3\nline4\nline5\n", encoding="utf-8", newline="\n" - ) - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all - ) - try: - message = await session.send_and_wait( - "Read lines 2 through 4 of the file 'lines.txt' in this directory. " - "Tell me what those lines contain.", - timeout=SEND_TIMEOUT, - ) - content = message.data.content if message else "" - assert "line2" in content - assert "line4" in content - finally: - await session.disconnect() - - async def test_should_handle_nonexistent_file_gracefully(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all - ) - try: - message = await session.send_and_wait( - "Try to read the file 'does_not_exist.txt'. " - "If it doesn't exist, say 'FILE_NOT_FOUND'.", - timeout=SEND_TIMEOUT, - ) - content = message.data.content if message else "" - assert re.search( - r"NOT.FOUND|NOT.EXIST|NO.SUCH|FILE_NOT_FOUND|DOES.NOT.EXIST|ERROR", - content, - re.IGNORECASE, - ) - finally: - await session.disconnect() - - async def test_should_edit_a_file_successfully(self, ctx: E2ETestContext): - Path(ctx.work_dir, "edit_me.txt").write_text( - "Hello World\nGoodbye World\n", encoding="utf-8", newline="\n" - ) - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all - ) - try: - message = await session.send_and_wait( - "Edit the file 'edit_me.txt': replace 'Hello World' with " - "'Hi Universe'. Then read it back and tell me its contents.", - timeout=SEND_TIMEOUT, - ) - assert message is not None - assert "Hi Universe" in message.data.content - finally: - await session.disconnect() - - async def test_should_create_a_new_file(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all - ) - try: - message = await session.send_and_wait( - "Create a file called 'new_file.txt' with the content " - "'Created by test'. Then read it back to confirm.", - timeout=SEND_TIMEOUT, - ) - assert message is not None - assert "Created by test" in message.data.content - finally: - await session.disconnect() - - async def test_should_search_for_patterns_in_files(self, ctx: E2ETestContext): - Path(ctx.work_dir, "data.txt").write_text( - "apple\nbanana\napricot\ncherry\n", encoding="utf-8", newline="\n" - ) - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all - ) - try: - message = await session.send_and_wait( - "Search for lines starting with 'ap' in the file 'data.txt'. " - "Tell me which lines matched.", - timeout=SEND_TIMEOUT, - ) - content = message.data.content if message else "" - assert "apple" in content - assert "apricot" in content - finally: - await session.disconnect() - - async def test_should_find_files_by_pattern(self, ctx: E2ETestContext): - src_dir = Path(ctx.work_dir, "src") - src_dir.mkdir() - Path(src_dir, "index.ts").write_text("export const index = 1;", encoding="utf-8") - Path(ctx.work_dir, "README.md").write_text("# Readme", encoding="utf-8") - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all - ) - try: - message = await session.send_and_wait( - "Find all .ts files in this directory (recursively). List the filenames you found.", - timeout=SEND_TIMEOUT, - ) - assert message is not None - assert "index.ts" in message.data.content - finally: - await session.disconnect() diff --git a/python/e2e/test_client_e2e.py b/python/e2e/test_client_e2e.py index a42c31885c..bc7da64485 100644 --- a/python/e2e/test_client_e2e.py +++ b/python/e2e/test_client_e2e.py @@ -12,7 +12,7 @@ ) from copilot.session import PermissionHandler -from .testharness import CLI_PATH +from .testharness import CLI_PATH, DEFAULT_GITHUB_TOKEN, E2ETestContext class TestClient: @@ -114,56 +114,60 @@ async def test_should_get_auth_status(self): await client.force_stop() @pytest.mark.asyncio - async def test_should_list_models_when_authenticated(self): - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + async def test_should_list_models_when_authenticated(self, ctx: E2ETestContext): + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + env=ctx.get_env(), + working_directory=ctx.work_dir, + github_token=DEFAULT_GITHUB_TOKEN, + ) try: await client.start() auth_status = await client.get_auth_status() - if not auth_status.isAuthenticated: - # Skip if not authenticated - models.list requires auth - await client.stop() - return + assert auth_status.isAuthenticated models = await client.list_models() assert isinstance(models, list) - if len(models) > 0: - model = models[0] - assert hasattr(model, "id") - assert hasattr(model, "name") - assert hasattr(model, "capabilities") - assert hasattr(model.capabilities, "supports") - assert hasattr(model.capabilities, "limits") + assert models + model = models[0] + assert hasattr(model, "id") + assert hasattr(model, "name") + assert hasattr(model, "capabilities") + assert hasattr(model.capabilities, "supports") + assert hasattr(model.capabilities, "limits") await client.stop() finally: await client.force_stop() @pytest.mark.asyncio - async def test_should_cache_models_list(self): + async def test_should_cache_models_list(self, ctx: E2ETestContext): """Test that list_models caches results to avoid rate limiting""" - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + env=ctx.get_env(), + working_directory=ctx.work_dir, + github_token=DEFAULT_GITHUB_TOKEN, + ) try: await client.start() auth_status = await client.get_auth_status() - if not auth_status.isAuthenticated: - # Skip if not authenticated - models.list requires auth - await client.stop() - return + assert auth_status.isAuthenticated # First call should fetch from backend models1 = await client.list_models() assert isinstance(models1, list) + assert models1 # Second call should return from cache (different list object but same content) models2 = await client.list_models() assert models2 is not models1, "Should return a copy, not the same object" assert len(models2) == len(models1), "Cached results should have same content" - if len(models1) > 0: - assert models1[0].id == models2[0].id, "Cached models should match" + assert models1[0].id == models2[0].id, "Cached models should match" # After stopping, cache should be cleared await client.stop() @@ -173,9 +177,7 @@ async def test_should_cache_models_list(self): # Check authentication again after restart auth_status = await client.get_auth_status() - if not auth_status.isAuthenticated: - await client.stop() - return + assert auth_status.isAuthenticated models3 = await client.list_models() assert models3 is not models1, "Cache should be cleared after disconnect" diff --git a/python/e2e/test_compaction_e2e.py b/python/e2e/test_compaction_e2e.py index 73df548839..736ccdb2da 100644 --- a/python/e2e/test_compaction_e2e.py +++ b/python/e2e/test_compaction_e2e.py @@ -1,6 +1,7 @@ """E2E Compaction Tests""" import asyncio +import os import pytest @@ -99,9 +100,27 @@ def _on_compaction_event(event): assert complete_event.data.compaction_tokens_used is not None, ( "Expected compaction tokens-used data" ) - assert (complete_event.data.compaction_tokens_used.input_tokens or 0) > 0, ( - "Expected compaction call to consume input tokens" - ) + # Replay has no prompt usage; recording must preserve the provider's value or absence. + if os.environ.get("GITHUB_ACTIONS") == "true": + assert (complete_event.data.compaction_tokens_used.input_tokens or 0) == 0, ( + "Expected replay compaction input tokens to be zero or unavailable" + ) + else: + exchanges = await ctx.get_exchanges() + compactions = [ + exchange["compactionUsage"] + for exchange in exchanges + if exchange.get("compactionUsage") + and exchange["compactionUsage"]["summary"] == complete_event.data.summary_content + ] + assert len(compactions) == 1, ( + f"Expected one provider compaction chain for the summary, got {compactions!r}" + ) + expected_input_tokens = compactions[0].get("inputTokens") + actual_input_tokens = complete_event.data.compaction_tokens_used.input_tokens + assert actual_input_tokens == expected_input_tokens, ( + f"Expected provider input tokens {expected_input_tokens}, got {actual_input_tokens}" + ) summary = (complete_event.data.summary_content or "").lower() assert "" in summary, "Expected summary to contain " assert "" in summary, "Expected summary to contain " diff --git a/python/e2e/test_inprocess_ffi_e2e.py b/python/e2e/test_inprocess_ffi_e2e.py index ea82037b7a..89565d414e 100644 --- a/python/e2e/test_inprocess_ffi_e2e.py +++ b/python/e2e/test_inprocess_ffi_e2e.py @@ -20,7 +20,10 @@ class TestInProcessFfi: - async def test_should_start_and_connect_over_in_process_ffi(self, ctx: E2ETestContext): + @pytest.mark.parametrize("shutdown_method", ["stop", "force_stop"]) + async def test_should_start_and_connect_over_in_process_ffi( + self, ctx: E2ETestContext, shutdown_method: str + ): # In-process hosting loads runtime.node directly. ``ping`` is a purely local # RPC round-trip, so no auth or replay proxy is involved. client = CopilotClient(connection=RuntimeConnection.for_inprocess()) @@ -31,4 +34,4 @@ async def test_should_start_and_connect_over_in_process_ffi(self, ctx: E2ETestCo assert pong.message == "pong: ffi message" assert pong.timestamp is not None finally: - await client.stop() + await getattr(client, shutdown_method)() diff --git a/python/e2e/test_mcp_oauth_e2e.py b/python/e2e/test_mcp_oauth_e2e.py index 202c842faa..9b3b265ddc 100644 --- a/python/e2e/test_mcp_oauth_e2e.py +++ b/python/e2e/test_mcp_oauth_e2e.py @@ -9,12 +9,9 @@ import pytest from copilot.generated.rpc import ( - GitHubTokenAcquireResultKind, MCPAppsCallToolRequest, MCPListToolsRequest, - MCPOauthHandlePendingRequest, MCPOauthLoginRequest, - MCPOauthPendingRequestResponse, ) from copilot.session import MCPHTTPServerConfig, MCPServerConfig, PermissionHandler from copilot.session_events import McpServerStatus @@ -195,85 +192,6 @@ def on_mcp_auth_request(request, _invocation): finally: await _stop_process(process) - async def test_should_resolve_pending_mcp_oauth_request_with_direct_rpc( - self, ctx: E2ETestContext - ): - url, process = await _start_oauth_mcp_server() - server_name = "oauth-direct-rpc-mcp" - observed_requests = asyncio.Queue() - release_handler = asyncio.Event() - - async def on_mcp_auth_request(request, _invocation): - observed_requests.put_nowait(request) - await release_handler.wait() - return {"kind": "token", "accessToken": EXPECTED_TOKEN} - - try: - mcp_servers: dict[str, MCPServerConfig] = { - server_name: { - "type": "http", - "url": f"{url}/mcp", - "tools": ["*"], - "oauthClientId": "sdk-e2e-client", - "oauthPublicClient": True, - } - } - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - on_mcp_auth_request=on_mcp_auth_request, - mcp_servers=mcp_servers, - enable_mcp_apps=True, - ) as session: - # session.create can begin MCP startup before the SDK registers OAuth - # event interest. Reload after registration so this test cannot lose - # the initial challenge to that race. - reload_task = asyncio.create_task(session.rpc.mcp.reload()) - connected = asyncio.create_task(_wait_for_mcp_server_status(session, server_name)) - try: - request = await asyncio.wait_for(observed_requests.get(), timeout=30.0) - while True: - handled = await session.rpc.mcp.oauth.handle_pending_request( - MCPOauthHandlePendingRequest( - request_id=request["requestId"], - result=MCPOauthPendingRequestResponse( - kind=GitHubTokenAcquireResultKind.TOKEN, - access_token=EXPECTED_TOKEN, - token_type="Bearer", - expires_in=3600, - ), - ) - ) - if handled.success: - break - request = await asyncio.wait_for(observed_requests.get(), timeout=30.0) - - assert request["serverName"] == server_name - assert request["serverUrl"] == f"{url}/mcp" - assert request["reason"] == "initial" - assert request["wwwAuthenticateParams"] == { - "resourceMetadataUrl": f"{url}/.well-known/oauth-protected-resource", - "scope": "mcp.read", - "error": "invalid_token", - } - - release_handler.set() - await asyncio.wait_for(reload_task, timeout=60.0) - connected_result = await asyncio.wait_for(connected, timeout=60.0) - assert connected_result is None - tools = await session.rpc.mcp.list_tools( - MCPListToolsRequest(server_name=server_name) - ) - assert [tool.name for tool in tools.tools] == ["whoami"] - finally: - release_handler.set() - if not connected.done(): - connected.cancel() - if not reload_task.done(): - reload_task.cancel() - await asyncio.gather(connected, reload_task, return_exceptions=True) - finally: - await _stop_process(process) - async def test_should_request_replacement_tokens_across_mcp_oauth_lifecycle( self, ctx: E2ETestContext ): diff --git a/python/e2e/test_pending_work_resume_e2e.py b/python/e2e/test_pending_work_resume_e2e.py index f0ac9c5481..eac29508b2 100644 --- a/python/e2e/test_pending_work_resume_e2e.py +++ b/python/e2e/test_pending_work_resume_e2e.py @@ -18,10 +18,9 @@ from copilot.rpc import ( HandlePendingToolCallRequest, PermissionDecisionRequest, - PermissionDecisionUserNotAvailable, SessionsCheckInUseRequest, ) -from copilot.session import PermissionHandler +from copilot.session import CopilotSession, PermissionHandler, PermissionNoResult from copilot.session_events import ExternalToolRequestedData, PermissionRequestedData from copilot.tools import Tool, ToolInvocation, ToolResult @@ -87,6 +86,26 @@ async def _safe_force_stop(client: CopilotClient) -> None: await client.force_stop() +async def _pending_tool_diagnostics(session: CopilotSession, request_id: str) -> str: + try: + events = await asyncio.wait_for(session.get_events(), PENDING_WORK_TIMEOUT) + except Exception as error: + return f"Rejected request {request_id}; history unavailable: {type(error).__name__}" + # Keep payloads out of failure reports while preserving request/turn ordering. + recent = [ + { + "type": event.type.value, + "id": event.id, + "ephemeral": event.ephemeral, + "request_id": getattr(event.data, "request_id", None), + "tool_call_id": getattr(event.data, "tool_call_id", None), + "success": getattr(event.data, "success", None), + } + for event in events[-32:] + ] + return f"Rejected request {request_id}; session {session.session_id}; recent events: {recent}" + + class TestPendingWorkResume: async def test_should_continue_pending_permission_request_after_resume( self, ctx: E2ETestContext @@ -145,7 +164,8 @@ def resumed_tool_handler(args): try: session2 = await resumed_client.resume_session( session_id, - on_permission_request=lambda req, inv: PermissionDecisionUserNotAvailable(), + # The manual approval below owns this response. + on_permission_request=lambda req, inv: PermissionNoResult(), continue_pending_work=True, tools=[_make_pending_tool("resume_permission_tool", resumed_tool_handler)], ) @@ -169,7 +189,7 @@ def resumed_tool_handler(args): permission_event_task.cancel() await asyncio.gather(permission_event_task, return_exceptions=True) if not release_original.done(): - release_original.set_result(PermissionDecisionUserNotAvailable()) + release_original.set_result(PermissionNoResult()) finally: await _safe_force_stop(server) @@ -340,14 +360,18 @@ async def tool_b(args): result="PARALLEL_B_BETA", ) ) - assert result_b.success + assert result_b.success, await _pending_tool_diagnostics( + session2, tool_b_event.data.request_id + ) result_a = await session2.rpc.tools.handle_pending_tool_call( HandlePendingToolCallRequest( request_id=tool_a_event.data.request_id, result="PARALLEL_A_ALPHA", ) ) - assert result_a.success + assert result_a.success, await _pending_tool_diagnostics( + session2, tool_a_event.data.request_id + ) await session2.disconnect() finally: diff --git a/python/e2e/test_provider_endpoint_e2e.py b/python/e2e/test_provider_endpoint_e2e.py deleted file mode 100644 index 875a95b91b..0000000000 --- a/python/e2e/test_provider_endpoint_e2e.py +++ /dev/null @@ -1,117 +0,0 @@ -"""E2E tests for session.provider.getEndpoint.""" - -# session.provider.getEndpoint is gated behind COPILOT_ALLOW_GET_PROVIDER_ENDPOINT; -# the harness env passed to the CLI subprocess opts in for this test file. - -import re - -import pytest - -from copilot.client import CopilotClient, RuntimeConnection -from copilot.generated.rpc import ProviderEndpointType, ProviderEndpointWireApi -from copilot.session import PermissionHandler - -from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -@pytest.fixture(scope="module") -async def provider_ctx(ctx: E2ETestContext): - env = {**ctx.get_env(), "COPILOT_ALLOW_GET_PROVIDER_ENDPOINT": "true"} - client = CopilotClient( - connection=RuntimeConnection.for_stdio(path=ctx.cli_path), - working_directory=ctx.work_dir, - env=env, - github_token=env["GITHUB_TOKEN"], - ) - try: - yield ctx, client - finally: - await client.stop() - - -class TestProviderEndpoint: - async def test_returns_byok_provider_endpoint_when_custom_provider_is_configured( - self, provider_ctx: tuple[E2ETestContext, CopilotClient] - ): - _, client = provider_ctx - session = await client.create_session( - on_permission_request=PermissionHandler.approve_all, - provider={ - "type": "openai", - "wire_api": "completions", - "base_url": "https://api.example.test/v1", - "api_key": "byok-secret", - "headers": {"X-Custom-Header": "byok-yes"}, - }, - ) - - try: - endpoint = await session.rpc.provider.get_endpoint() - - assert endpoint.type == ProviderEndpointType.OPENAI - assert endpoint.wire_api == ProviderEndpointWireApi.COMPLETIONS - assert endpoint.base_url == "https://api.example.test/v1" - assert endpoint.api_key == "byok-secret" - assert endpoint.headers["X-Custom-Header"] == "byok-yes" - # BYOK sessions never issue a CAPI session token. - assert endpoint.session_token is None - finally: - try: - await session.disconnect() - except Exception: - pass # disconnect may fail since the BYOK provider URL is fake - - async def test_returns_capi_provider_endpoint_for_oauth_authenticated_session( - self, provider_ctx: tuple[E2ETestContext, CopilotClient] - ): - _, client = provider_ctx - session = await client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - - try: - endpoint = await session.rpc.provider.get_endpoint() - - assert endpoint.type in ( - ProviderEndpointType.OPENAI, - ProviderEndpointType.AZURE, - ProviderEndpointType.ANTHROPIC, - ) - # wire_api is omitted for anthropic; otherwise one of the OpenAI shapes. - if endpoint.type != ProviderEndpointType.ANTHROPIC: - assert endpoint.wire_api in ( - ProviderEndpointWireApi.COMPLETIONS, - ProviderEndpointWireApi.RESPONSES, - ) - - # CAPI baseUrl is the (proxy) Copilot API URL injected by the harness. - assert re.match(r"^https?://", endpoint.base_url) - - # For CAPI OAuth sessions the api_key is the resolved GitHub bearer. - assert isinstance(endpoint.api_key, str) - assert len(endpoint.api_key) > 0 - - # Standard CAPI headers must be present, and Authorization is - # surfaced as the runtime sends it (`Bearer `). - assert isinstance(endpoint.headers["Copilot-Integration-Id"], str) - assert re.search(r"Copilot", endpoint.headers["User-Agent"], re.IGNORECASE) - assert isinstance(endpoint.headers["X-GitHub-Api-Version"], str) - assert re.search(r"[0-9a-f-]{8,}", endpoint.headers["X-Interaction-Id"]) - assert endpoint.headers["Authorization"] == f"Bearer {endpoint.api_key}" - - # When the omit-model_id path returned an auto-mode session token, - # it must use the documented header name. The harness may have a - # non-auto model selected, in which case the field is simply - # omitted. - if endpoint.session_token is not None: - assert endpoint.session_token.header == "Copilot-Session-Token" - assert len(endpoint.session_token.token) > 0 - # When provided, expires_at should be a parseable ISO timestamp. - if endpoint.session_token.expires_at is not None: - from datetime import datetime - - datetime.fromisoformat(endpoint.session_token.expires_at.replace("Z", "+00:00")) - finally: - await session.disconnect() diff --git a/python/e2e/test_rewind_e2e.py b/python/e2e/test_rewind_e2e.py deleted file mode 100644 index cf7627c1a3..0000000000 --- a/python/e2e/test_rewind_e2e.py +++ /dev/null @@ -1,108 +0,0 @@ -"""E2E coverage for rewinding tracked files and conversation history.""" - -from __future__ import annotations - -import asyncio -import os -from pathlib import Path - -import pytest - -from copilot.rpc import ( - HistoryPreviewRewindRequest, - HistoryRewindMode, - HistoryRewindOutcome, - HistoryRewindRequest, -) -from copilot.session import PermissionHandler - -from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - -FILE_NAME = "rewind-sdk.txt" -ORIGINAL_FILE_CONTENT = "Original rewind content" -PREPARED_FILE_CONTENT = "Prepared rewind content" -FILE_CONTENT = "SDK rewind content" - - -def _same_path(left: str | Path, right: str | Path) -> bool: - return os.path.normcase(os.path.abspath(left)) == os.path.normcase(os.path.abspath(right)) - - -class TestRewind: - async def test_should_restore_tracked_file_and_conversation(self, ctx: E2ETestContext): - file_path = Path(ctx.work_dir) / FILE_NAME - file_path.write_text(ORIGINAL_FILE_CONTENT, encoding="utf-8") - session = await ctx.client.create_session( - model="claude-sonnet-5", - enable_file_change_tracking=True, - on_permission_request=PermissionHandler.approve_all, - ) - - try: - ready = await session.send_and_wait( - f"Use the edit tool to replace the exact contents of {FILE_NAME} " - f"from {ORIGINAL_FILE_CONTENT} to {PREPARED_FILE_CONTENT}. " - "After the tool succeeds, reply with exactly SDK_REWIND_READY." - ) - assert ready is not None - assert ready.data.content == "SDK_REWIND_READY" - assert file_path.read_text(encoding="utf-8") == PREPARED_FILE_CONTENT - - response = await session.send_and_wait( - f"Use the edit tool to replace the exact contents of {FILE_NAME} " - f"from {PREPARED_FILE_CONTENT} to {FILE_CONTENT}. " - "After the tool succeeds, reply with exactly SDK_REWIND_DONE." - ) - - assert response is not None - assert response.data.content == "SDK_REWIND_DONE" - assert file_path.read_text(encoding="utf-8") == FILE_CONTENT - - # File change capture settles asynchronously after the turn completes, so a - # rewind point can briefly report zero restorable files. Poll until the - # capture lands instead of sampling once; the assertions below still run if - # it never does. - rewind_points = await session.rpc.history.list_rewind_points() - deadline = asyncio.get_running_loop().time() + 30 - while asyncio.get_running_loop().time() < deadline and not ( - rewind_points.unavailable_reason is None - and len(rewind_points.points) == 2 - and rewind_points.points[1].turn_changed_files - and rewind_points.points[1].can_restore_files - ): - await asyncio.sleep(0.1) - rewind_points = await session.rpc.history.list_rewind_points() - - assert rewind_points.unavailable_reason is None - assert rewind_points.file_change_tracking_enabled - assert len(rewind_points.points) == 2 - rewind_point = rewind_points.points[1] - assert rewind_point.turn_changed_files - assert rewind_point.can_restore_files - assert rewind_point.file_count == 1 - - preview = await session.rpc.history.preview_rewind( - HistoryPreviewRewindRequest(event_id=rewind_point.event_id) - ) - assert preview.available - assert len(preview.files) == 1 - assert _same_path(preview.files[0].path, file_path) - - rewind = await session.rpc.history.rewind( - HistoryRewindRequest( - event_id=rewind_point.event_id, - mode=HistoryRewindMode.CONVERSATION_AND_FILES, - ) - ) - assert rewind.outcome == HistoryRewindOutcome.SUCCESS - assert rewind.events_removed is not None and rewind.events_removed > 0 - assert len(rewind.restored_files) == 1 - assert _same_path(rewind.restored_files[0], file_path) - assert file_path.read_text(encoding="utf-8") == PREPARED_FILE_CONTENT - - events = await session.get_events() - assert all(str(event.id) != rewind_point.event_id for event in events) - finally: - await session.disconnect() diff --git a/python/e2e/test_rpc_commands_e2e.py b/python/e2e/test_rpc_commands_e2e.py deleted file mode 100644 index 32fbc5b184..0000000000 --- a/python/e2e/test_rpc_commands_e2e.py +++ /dev/null @@ -1,117 +0,0 @@ -"""E2E coverage for session.commands RPC methods.""" - -from __future__ import annotations - -import pytest - -from copilot.rpc import ( - CommandsInvokeRequest, - CommandsRespondToQueuedCommandRequest, - ExecuteCommandParams, - QueuedCommandHandled, - SessionCommandsListRequest, - SlashCommandKind, - SlashCommandTextResult, -) -from copilot.session import CommandContext, CommandDefinition, PermissionHandler - -from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -class TestRpcCommands: - async def test_should_list_builtin_and_client_commands(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - commands=[ - CommandDefinition( - name="deploy", - description="Deploy the app", - handler=lambda _: None, - ) - ], - ) - try: - commands = await session.rpc.commands.list(SessionCommandsListRequest()) - by_name = {command.name: command for command in commands.commands} - - builtins = [ - command for command in commands.commands if command.kind == SlashCommandKind.BUILTIN - ] - assert builtins - if "model" in by_name: - assert by_name["model"].kind == SlashCommandKind.BUILTIN - if "compact" in by_name: - assert by_name["compact"].kind == SlashCommandKind.BUILTIN - - assert "deploy" in by_name - assert by_name["deploy"].kind == SlashCommandKind.CLIENT - assert by_name["deploy"].description == "Deploy the app" - finally: - await session.disconnect() - - async def test_should_invoke_builtin_model_command(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - result = await session.rpc.commands.invoke(CommandsInvokeRequest(name="model")) - assert result is not None - if isinstance(result, SlashCommandTextResult): - assert result.text.strip() - else: - assert getattr(result, "kind", None) in { - "agent-prompt", - "completed", - "select-subcommand", - "text", - } - finally: - await session.disconnect() - - async def test_should_execute_registered_command_with_arguments(self, ctx: E2ETestContext): - calls: list[CommandContext] = [] - - def deploy(context: CommandContext) -> None: - calls.append(context) - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - commands=[ - CommandDefinition( - name="deploy", - description="Deploy the app", - handler=deploy, - ) - ], - ) - try: - result = await session.rpc.commands.execute( - ExecuteCommandParams(command_name="deploy", args="production") - ) - assert result.error is None - assert len(calls) == 1 - assert calls[0].session_id == session.session_id - assert calls[0].command_name == "deploy" - assert calls[0].args == "production" - assert calls[0].command == "/deploy production" - finally: - await session.disconnect() - - async def test_should_return_false_for_unknown_queued_command_response( - self, ctx: E2ETestContext - ): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - result = await session.rpc.commands.respond_to_queued_command( - CommandsRespondToQueuedCommandRequest( - request_id="missing-queued-command", - result=QueuedCommandHandled(stop_processing_queue=True), - ) - ) - assert result.success is False - finally: - await session.disconnect() diff --git a/python/e2e/test_rpc_e2e.py b/python/e2e/test_rpc_e2e.py deleted file mode 100644 index 8edde6bf96..0000000000 --- a/python/e2e/test_rpc_e2e.py +++ /dev/null @@ -1,233 +0,0 @@ -"""E2E RPC Tests""" - -import pytest - -from copilot import CopilotClient, RuntimeConnection -from copilot.rpc import ( - ModelsListRequest, - PingRequest, -) -from copilot.session import PermissionHandler - -from .testharness import CLI_PATH, E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -class TestRpc: - @pytest.mark.asyncio - async def test_should_call_rpc_ping_with_typed_params(self): - """Test calling rpc.ping with typed params and result""" - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) - - try: - await client.start() - - result = await client.rpc.ping(PingRequest(message="typed rpc test")) - assert result.message == "pong: typed rpc test" - assert result.timestamp is not None - - await client.stop() - finally: - await client.force_stop() - - @pytest.mark.asyncio - async def test_should_call_rpc_models_list(self): - """Test calling rpc.models.list with typed result""" - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) - - try: - await client.start() - - auth_status = await client.get_auth_status() - if not auth_status.isAuthenticated: - await client.stop() - return - - result = await client.rpc.models.list(ModelsListRequest()) - assert result.models is not None - assert isinstance(result.models, list) - - await client.stop() - finally: - await client.force_stop() - - # account.getQuota is defined in schema but not yet implemented in CLI - @pytest.mark.skip(reason="account.getQuota not yet implemented in CLI") - @pytest.mark.asyncio - async def test_should_call_rpc_account_get_quota(self): - """Test calling rpc.account.getQuota when authenticated""" - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) - - try: - await client.start() - - auth_status = await client.get_auth_status() - if not auth_status.isAuthenticated: - await client.stop() - return - - result = await client.rpc.account.get_quota() - assert result.quota_snapshots is not None - assert isinstance(result.quota_snapshots, dict) - - await client.stop() - finally: - await client.force_stop() - - -class TestSessionRpc: - # session.model.getCurrent is defined in schema but not yet implemented in CLI - @pytest.mark.skip(reason="session.model.getCurrent not yet implemented in CLI") - async def test_should_call_session_rpc_model_get_current(self, ctx: E2ETestContext): - """Test calling session.rpc.model.getCurrent""" - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-5" - ) - - result = await session.rpc.model.get_current() - assert result.model_id is not None - assert isinstance(result.model_id, str) - - # session.model.switchTo is defined in schema but not yet implemented in CLI - @pytest.mark.skip(reason="session.model.switchTo not yet implemented in CLI") - async def test_should_call_session_rpc_model_switch_to(self, ctx: E2ETestContext): - """Test calling session.rpc.model.switchTo""" - from copilot.rpc import ModelSwitchToRequest - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-5" - ) - - before = await session.rpc.model.get_current() - assert before.model_id is not None - - # Switch to a different model with reasoning effort - result = await session.rpc.model.switch_to( - ModelSwitchToRequest(model_id="gpt-4.1", reasoning_effort="high") - ) - assert result.model_id == "gpt-4.1" - - # Verify the switch persisted - after = await session.rpc.model.get_current() - assert after.model_id == "gpt-4.1" - - @pytest.mark.asyncio - async def test_get_and_set_session_mode(self): - """Test getting and setting session mode""" - from copilot.rpc import ModeSetRequest - from copilot.session_events import SessionMode - - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) - - try: - await client.start() - session = await client.create_session( - on_permission_request=PermissionHandler.approve_all - ) - - # Get initial mode (default should be interactive) - initial = await session.rpc.mode.get() - assert initial == SessionMode.INTERACTIVE - - # Switch to plan mode - await session.rpc.mode.set(ModeSetRequest(mode=SessionMode.PLAN)) - - # Verify mode persisted - after_plan = await session.rpc.mode.get() - assert after_plan == SessionMode.PLAN - - # Switch back to interactive - await session.rpc.mode.set(ModeSetRequest(mode=SessionMode.INTERACTIVE)) - - await session.disconnect() - await client.stop() - finally: - await client.force_stop() - - @pytest.mark.asyncio - async def test_read_update_and_delete_plan(self): - """Test reading, updating, and deleting plan""" - from copilot.rpc import PlanUpdateRequest - - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) - - try: - await client.start() - session = await client.create_session( - on_permission_request=PermissionHandler.approve_all - ) - - # Initially plan should not exist - initial = await session.rpc.plan.read() - assert initial.exists is False - assert initial.content is None - - # Create/update plan - plan_content = "# Test Plan\n\n- Step 1\n- Step 2" - await session.rpc.plan.update(PlanUpdateRequest(content=plan_content)) - - # Verify plan exists and has correct content - after_update = await session.rpc.plan.read() - assert after_update.exists is True - assert after_update.content == plan_content - - await session.rpc.plan.delete() - - # Verify plan is deleted - after_delete = await session.rpc.plan.read() - assert after_delete.exists is False - assert after_delete.content is None - - await session.disconnect() - await client.stop() - finally: - await client.force_stop() - - @pytest.mark.asyncio - async def test_create_list_and_read_workspace_files(self): - """Test creating, listing, and reading workspace files""" - from copilot.rpc import ( - WorkspacesCreateFileRequest, - WorkspacesReadFileRequest, - ) - - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) - - try: - await client.start() - session = await client.create_session( - on_permission_request=PermissionHandler.approve_all - ) - - # Initially no files - initial_files = await session.rpc.workspaces.list_files() - assert initial_files.files == [] - - # Create a file - file_content = "Hello, workspace!" - await session.rpc.workspaces.create_file( - WorkspacesCreateFileRequest(content=file_content, path="test.txt") - ) - - # List files - after_create = await session.rpc.workspaces.list_files() - assert "test.txt" in after_create.files - - read_result = await session.rpc.workspaces.read_file( - WorkspacesReadFileRequest(path="test.txt") - ) - assert read_result.content == file_content - - await session.rpc.workspaces.create_file( - WorkspacesCreateFileRequest(content="Nested content", path="subdir/nested.txt") - ) - - after_nested = await session.rpc.workspaces.list_files() - assert "test.txt" in after_nested.files - assert any("nested.txt" in f for f in after_nested.files) - - await session.disconnect() - await client.stop() - finally: - await client.force_stop() diff --git a/python/e2e/test_rpc_event_log_e2e.py b/python/e2e/test_rpc_event_log_e2e.py deleted file mode 100644 index f17e940f46..0000000000 --- a/python/e2e/test_rpc_event_log_e2e.py +++ /dev/null @@ -1,191 +0,0 @@ -"""E2E coverage for session.eventLog RPC methods.""" - -from __future__ import annotations - -import asyncio -import time -import uuid -from collections.abc import Awaitable, Callable -from typing import TYPE_CHECKING - -import pytest - -from copilot.rpc import ( - EventLogReadRequest, - EventsCursorStatus, - NameSetRequest, - PlanUpdateRequest, - RegisterEventInterestParams, - ReleaseEventInterestParams, - SessionsSaveRequest, -) -from copilot.session import PermissionHandler -from copilot.session_events import ( - PlanChangedOperation, - SessionPlanChangedData, - SessionTitleChangedData, -) - -if TYPE_CHECKING: - from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -async def _wait_for( - predicate: Callable[[], Awaitable[bool]], - *, - timeout: float = 30.0, - message: str, -) -> None: - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - if await predicate(): - return - await asyncio.sleep(0.2) - pytest.fail(message) - - -class TestRpcEventLog: - async def test_should_read_persisted_events_from_beginning(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - await session.rpc.plan.update( - PlanUpdateRequest(content="# Event log E2E plan\n- persisted event") - ) - - observed = None - - async def has_plan_event() -> bool: - nonlocal observed - observed = await session.rpc.event_log.read(EventLogReadRequest(max=100, wait_ms=0)) - return any( - isinstance(evt.data, SessionPlanChangedData) - and evt.data.operation == PlanChangedOperation.CREATE - and evt.ephemeral is not True - for evt in observed.events - ) - - await _wait_for( - has_plan_event, - message="Timed out waiting for persisted session.plan_changed event.", - ) - - assert observed is not None - assert observed.cursor_status == EventsCursorStatus.OK - assert observed.cursor - assert any( - isinstance(evt.data, SessionPlanChangedData) - and evt.data.operation == PlanChangedOperation.CREATE - for evt in observed.events - ) - finally: - await session.disconnect() - - async def test_should_return_tail_cursor_and_read_empty_when_no_new_events( - self, ctx: E2ETestContext - ): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - tail = await session.rpc.event_log.tail() - await session.log("Ephemeral event after tail", ephemeral=True) - request = EventLogReadRequest( - cursor=tail.cursor, max=10, wait_ms=0, include_ephemeral=False - ) - read = await session.rpc.event_log.read(request) - - assert tail.cursor - assert read.cursor_status == EventsCursorStatus.OK - assert read.events == [] - assert read.has_more is False - - await session.rpc.plan.update(PlanUpdateRequest(content="# Durable event after tail")) - await ctx.client.rpc.sessions.save(SessionsSaveRequest(session_id=session.session_id)) - read = await session.rpc.event_log.read(request) - assert read.cursor_status == EventsCursorStatus.OK - assert any( - isinstance(evt.data, SessionPlanChangedData) - and evt.data.operation == PlanChangedOperation.CREATE - and evt.ephemeral is not True - for evt in read.events - ) - finally: - await session.disconnect() - - async def test_should_register_and_release_event_interest_idempotently( - self, ctx: E2ETestContext - ): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - registered = await session.rpc.event_log.register_interest( - RegisterEventInterestParams(event_type="session.title_changed") - ) - assert registered.handle - - released = await session.rpc.event_log.release_interest( - ReleaseEventInterestParams(handle=registered.handle) - ) - assert released.success is True - - released_again = await session.rpc.event_log.release_interest( - ReleaseEventInterestParams(handle=registered.handle) - ) - assert released_again.success is True - finally: - await session.disconnect() - - async def test_should_long_poll_with_types_filter_for_title_changed_event( - self, ctx: E2ETestContext - ): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - expected_title = f"EventLogTitle-{uuid.uuid4().hex}" - tail = await session.rpc.event_log.tail() - last_read = None - - async def read_until_title(): - nonlocal last_read - cursor = tail.cursor - while True: - last_read = await session.rpc.event_log.read( - EventLogReadRequest( - cursor=cursor, - max=10, - wait_ms=5000, - types=["session.title_changed"], - ) - ) - assert last_read.cursor_status == EventsCursorStatus.OK - assert all( - evt.type.value == "session.title_changed" for evt in last_read.events - ) - if any( - isinstance(evt.data, SessionTitleChangedData) - and evt.data.title == expected_title - for evt in last_read.events - ): - return - # Any new event can wake a long poll, even if the filter removes it. - cursor = last_read.cursor - - read_task = asyncio.create_task(read_until_title()) - try: - await session.rpc.name.set(NameSetRequest(name=expected_title)) - try: - await asyncio.wait_for(read_task, timeout=10.0) - except TimeoutError as exc: - exc.add_note(f"Expected title {expected_title!r}; last batch: {last_read!r}") - raise - finally: - read_task.cancel() - await asyncio.gather(read_task, return_exceptions=True) - finally: - await session.disconnect() diff --git a/python/e2e/test_rpc_event_side_effects_e2e.py b/python/e2e/test_rpc_event_side_effects_e2e.py deleted file mode 100644 index c759be2657..0000000000 --- a/python/e2e/test_rpc_event_side_effects_e2e.py +++ /dev/null @@ -1,268 +0,0 @@ -""" -E2E coverage for session-event side effects triggered by RPC calls. - -Mirrors ``dotnet/test/RpcEventSideEffectsE2ETests.cs`` (snapshot category -``rpc_event_side_effects``). -""" - -from __future__ import annotations - -import asyncio - -import pytest - -from copilot.rpc import ( - HistoryTruncateRequest, - ModeSetRequest, - NameSetRequest, - PlanUpdateRequest, - WorkspacesCreateFileRequest, -) -from copilot.session import PermissionHandler -from copilot.session_events import ( - PlanChangedOperation, - SessionMode, - SessionModeChangedData, - SessionPlanChangedData, - SessionSnapshotRewindData, - SessionTitleChangedData, - SessionWorkspaceFileChangedData, - WorkspaceFileChangedOperation, -) - -from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -class TestRpcEventSideEffects: - async def test_should_emit_mode_changed_event_when_mode_set(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - changed_future: asyncio.Future = asyncio.get_event_loop().create_future() - - def on_event(event): - if isinstance(event.data, SessionModeChangedData) and not changed_future.done(): - changed_future.set_result(event) - - unsubscribe = session.on(on_event) - try: - await session.rpc.mode.set(ModeSetRequest(mode=SessionMode.PLAN)) - event = await asyncio.wait_for(changed_future, timeout=15.0) - - assert isinstance(event.data, SessionModeChangedData) - assert event.data.new_mode == SessionMode.PLAN - assert event.data.previous_mode == SessionMode.INTERACTIVE - finally: - unsubscribe() - finally: - await session.disconnect() - - async def test_should_emit_plan_changed_event_for_update_and_delete(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - create_future: asyncio.Future = asyncio.get_event_loop().create_future() - delete_future: asyncio.Future = asyncio.get_event_loop().create_future() - - def on_event(event): - if isinstance(event.data, SessionPlanChangedData): - if ( - event.data.operation == PlanChangedOperation.CREATE - and not create_future.done() - ): - create_future.set_result(event) - elif ( - event.data.operation == PlanChangedOperation.DELETE - and not delete_future.done() - ): - delete_future.set_result(event) - - unsubscribe = session.on(on_event) - try: - await session.rpc.plan.update(PlanUpdateRequest(content="# Plan step 1")) - create_evt = await asyncio.wait_for(create_future, timeout=15.0) - assert create_evt.data.operation == PlanChangedOperation.CREATE - - await session.rpc.plan.delete() - delete_evt = await asyncio.wait_for(delete_future, timeout=15.0) - assert delete_evt.data.operation == PlanChangedOperation.DELETE - finally: - unsubscribe() - finally: - await session.disconnect() - - async def test_should_emit_plan_changed_update_operation_on_second_update( - self, ctx: E2ETestContext - ): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - # Create the plan first - await session.rpc.plan.update(PlanUpdateRequest(content="# Initial plan")) - - update_future: asyncio.Future = asyncio.get_event_loop().create_future() - - def on_event(event): - if ( - isinstance(event.data, SessionPlanChangedData) - and event.data.operation == PlanChangedOperation.UPDATE - and not update_future.done() - ): - update_future.set_result(event) - - unsubscribe = session.on(on_event) - try: - await session.rpc.plan.update(PlanUpdateRequest(content="# Updated plan")) - update_evt = await asyncio.wait_for(update_future, timeout=15.0) - assert update_evt.data.operation == PlanChangedOperation.UPDATE - finally: - unsubscribe() - finally: - await session.disconnect() - - async def test_should_emit_workspace_file_changed_event_when_file_created( - self, ctx: E2ETestContext - ): - import uuid - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - path = f"event-side-effect-{uuid.uuid4().hex}.txt" - create_future: asyncio.Future = asyncio.get_event_loop().create_future() - - def on_event(event): - if ( - isinstance(event.data, SessionWorkspaceFileChangedData) - and event.data.path == path - and event.data.operation == WorkspaceFileChangedOperation.CREATE - and not create_future.done() - ): - create_future.set_result(event) - - unsubscribe = session.on(on_event) - try: - await session.rpc.workspaces.create_file( - WorkspacesCreateFileRequest(path=path, content="hello") - ) - evt = await asyncio.wait_for(create_future, timeout=15.0) - assert evt.data.path == path - assert evt.data.operation == WorkspaceFileChangedOperation.CREATE - finally: - unsubscribe() - finally: - await session.disconnect() - - async def test_should_emit_title_changed_event_when_name_set(self, ctx: E2ETestContext): - import uuid - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - new_name = f"Title-{uuid.uuid4().hex}" - title_future: asyncio.Future = asyncio.get_event_loop().create_future() - - def on_event(event): - if ( - isinstance(event.data, SessionTitleChangedData) - and event.data.title == new_name - and not title_future.done() - ): - title_future.set_result(event) - - unsubscribe = session.on(on_event) - try: - await session.rpc.name.set(NameSetRequest(name=new_name)) - evt = await asyncio.wait_for(title_future, timeout=15.0) - assert evt.data.title == new_name - finally: - unsubscribe() - finally: - await session.disconnect() - - async def test_should_emit_snapshot_rewind_event_and_remove_events_on_truncate( - self, ctx: E2ETestContext - ): - """Truncating history emits a session.snapshot_rewind event.""" - from copilot.session_events import UserMessageData - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - await session.send_and_wait("Say SNAPSHOT_REWIND_TARGET exactly.", timeout=60.0) - - events = await session.get_events() - user_msgs = [e for e in events if isinstance(e.data, UserMessageData)] - assert len(user_msgs) >= 1 - first_user_event_id = str(user_msgs[0].id) - - rewind_future: asyncio.Future = asyncio.get_event_loop().create_future() - - def on_event(event): - if isinstance(event.data, SessionSnapshotRewindData) and not rewind_future.done(): - rewind_future.set_result(event) - - unsubscribe = session.on(on_event) - try: - await session.rpc.history.truncate( - HistoryTruncateRequest(event_id=first_user_event_id) - ) - evt = await asyncio.wait_for(rewind_future, timeout=15.0) - assert isinstance(evt.data, SessionSnapshotRewindData) - assert evt.data.events_removed >= 1 - assert evt.data.up_to_event_id.lower() == first_user_event_id.lower() - - messages_after = await session.get_events() - assert not any(e.id == user_msgs[0].id for e in messages_after) - except Exception as exc: - if "unhandled method" in str(exc).lower(): - pytest.skip("session.history.truncate not supported in this CLI build") - raise - finally: - unsubscribe() - finally: - await session.disconnect() - - async def test_should_allow_session_use_after_truncate(self, ctx: E2ETestContext): - """Session remains usable after history truncation.""" - from copilot.session_events import UserMessageData - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - await session.send_and_wait("Say SNAPSHOT_REWIND_TARGET exactly.", timeout=60.0) - - events = await session.get_events() - user_msgs = [e for e in events if isinstance(e.data, UserMessageData)] - assert len(user_msgs) >= 1 - first_user_event_id = str(user_msgs[0].id) - - try: - truncate_result = await session.rpc.history.truncate( - HistoryTruncateRequest(event_id=first_user_event_id) - ) - assert truncate_result.events_removed >= 1 - except Exception as exc: - if "unhandled method" in str(exc).lower(): - pytest.skip("session.history.truncate not supported in this CLI build") - raise - - mode = await session.rpc.mode.get() - assert mode in ( - SessionMode.INTERACTIVE, - SessionMode.PLAN, - SessionMode.AUTOPILOT, - ) - workspace = await session.rpc.workspaces.get_workspace() - assert workspace is not None - finally: - await session.disconnect() diff --git a/python/e2e/test_rpc_generated_surface_e2e.py b/python/e2e/test_rpc_generated_surface_e2e.py deleted file mode 100644 index 3e3f9290fd..0000000000 --- a/python/e2e/test_rpc_generated_surface_e2e.py +++ /dev/null @@ -1,857 +0,0 @@ -"""Offline E2E coverage for generated outbound RPC methods.""" - -from __future__ import annotations - -import dataclasses -import enum -import inspect -import json -import os -import types -import typing -from datetime import UTC, datetime -from pathlib import Path -from uuid import UUID - -import pytest - -from copilot import CopilotClient, RuntimeConnection, rpc -from copilot._jsonrpc import JsonRpcError -from copilot.generated import session_events as generated_session_events -from copilot.session import CopilotSession - -from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - -_SESSION_ID = "generated-rpc-surface-session" -_SAMPLE_UUID = UUID("12345678-1234-5678-1234-567812345678") -_OBJECTIVE_METHODS = { - "session.workspaces.readAutopilotObjective", - "session.workspaces.writeAutopilotObjective", - "session.workspaces.deleteAutopilotObjective", - "session.workspaces.autopilotObjectiveExists", -} - -_GAP_METHODS = { - "registerExtensionLaunchProvider": rpc.ServerRpc.register_extension_launch_provider, - "hooks.discover": rpc.ServerHooksApi.discover, - "models.getBuiltInCatalog": rpc.ServerModelsApi.get_built_in_catalog, - "mcp.planInstall": rpc.ServerMcpApi.plan_install, - "extensions.discover": rpc.ServerExtensionsApi.discover, - "extensions.enable": rpc.ServerExtensionsApi.enable, - "extensions.disable": rpc.ServerExtensionsApi.disable, - "catalog.search": rpc.ServerCatalogApi.search, - "plugins.builtin.set": rpc.ServerPluginsBuiltinApi.set, - "skills.config.setSkillDisabled": rpc.ServerSkillsConfigApi.set_skill_disabled, - "commands.list": rpc.ServerCommandsApi.list, - "managedSettings.read": rpc.ServerManagedSettingsApi.read, - "llmInference.setProvider": rpc.ServerLlmInferenceApi.set_provider, - "sessions.getClientMetadata": rpc.ServerSessionsApi.get_client_metadata, - "sessions.readPersistedEvents": rpc.ServerSessionsApi.read_persisted_events, - "session.send": rpc.SessionRpc.send, - "session.sendMessages": rpc.SessionRpc.send_messages, - "session.abort": rpc.SessionRpc.abort, - "session.interruptMainTurn": rpc.SessionRpc.interrupt_main_turn, - "session.cancelAllBackgroundAgents": rpc.SessionRpc.cancel_all_background_agents, - "session.log": rpc.SessionRpc.log, - "session.sandbox.getEnforcementStatus": rpc.SandboxApi.get_enforcement_status, - "session.sandbox.disableForSession": rpc.SandboxApi.disable_for_session, - "session.debug.collectLogs": rpc.DebugApi.collect_logs, - "session.workflow.run": rpc.WorkflowApi.run, - "session.workflow.resume": rpc.WorkflowApi.resume, - "session.workflow.getRun": rpc.WorkflowApi.get_run, - "session.workflow.listRuns": rpc.WorkflowApi.list_runs, - "session.workflow.getRunDetail": rpc.WorkflowApi.get_run_detail, - "session.workflow.getRunProgress": rpc.WorkflowApi.get_run_progress, - "session.workflow.cancel": rpc.WorkflowApi.cancel, - "session.workflow.pause": rpc.WorkflowApi.pause, - "session.workflow.log": rpc.WorkflowApi.log, - "session.workflow.agent": rpc.WorkflowApi.agent, - "session.workflow.journal.get": rpc.WorkflowJournalApi.get, - "session.workflow.journal.put": rpc.WorkflowJournalApi.put, - "session.model.switchAutoTier": rpc.ModelApi.switch_auto_tier, - "session.model.setAllowedModels": rpc.ModelApi.set_allowed_models, - "session.workspaces.updateMetadata": rpc.WorkspacesApi.update_metadata, - "session.workspaces.ensure": rpc.WorkspacesApi.ensure, - "session.workspaces.statFile": rpc.WorkspacesApi.stat_file, - "session.workspaces.createDirectory": rpc.WorkspacesApi.create_directory, - "session.workspaces.removePath": rpc.WorkspacesApi.remove_path, - "session.workspaces.renamePath": rpc.WorkspacesApi.rename_path, - "session.workspaces.addSummary": rpc.WorkspacesApi.add_summary, - "session.workspaces.truncateSummaries": rpc.WorkspacesApi.truncate_summaries, - "session.workspaces.readAutopilotObjective": rpc.WorkspacesApi.read_autopilot_objective, - "session.workspaces.writeAutopilotObjective": rpc.WorkspacesApi.write_autopilot_objective, - "session.workspaces.deleteAutopilotObjective": rpc.WorkspacesApi.delete_autopilot_objective, - "session.workspaces.autopilotObjectiveExists": (rpc.WorkspacesApi.autopilot_objective_exists), - "session.autopilotObjective.getState": rpc.AutopilotObjectiveApi.get_state, - "session.agent.setPrompt": rpc.AgentApi.set_prompt, - "session.tasks.register": rpc.TasksApi.register, - "session.tasks.update": rpc.TasksApi.update, - "session.mcp.moveLoadingToBackground": rpc.McpApi.move_loading_to_background, - "session.mcp.startServer": rpc.McpApi.start_server, - "session.mcp.restartServer": rpc.McpApi.restart_server, - "session.mcp.oauth.authenticationStateChanged": (rpc.McpOauthApi.authentication_state_changed), - "session.mcp.oauth.probe": rpc.McpOauthApi.probe, - "session.mcp.oauth.respond": rpc.McpOauthApi.respond, - "session.mcp.resources.read": rpc.McpResourcesApi.read, - "session.mcp.resources.list": rpc.McpResourcesApi.list, - "session.mcp.resources.listTemplates": rpc.McpResourcesApi.list_templates, - "session.tools.execute": rpc.ToolsApi.execute, - "session.tools.getBuiltinDescriptors": rpc.ToolsApi.get_builtin_descriptors, - "session.tools.taskCompleteEventData": rpc.ToolsApi.task_complete_event_data, - "session.tools.set": rpc.ToolsApi.set, - "session.permissions.configure": rpc.PermissionsApi.configure, - "session.permissions.pendingRequests": rpc.PermissionsApi.pending_requests, - "session.permissions.modifyRules": rpc.PermissionsApi.modify_rules, - "session.permissions.setRequired": rpc.PermissionsApi.set_required, - "session.permissions.notifyPromptShown": rpc.PermissionsApi.notify_prompt_shown, - "session.permissions.paths.list": rpc.PermissionsPathsApi.list, - "session.permissions.paths.add": rpc.PermissionsPathsApi.add, - "session.permissions.paths.updatePrimary": rpc.PermissionsPathsApi.update_primary, - "session.permissions.paths.isPathWithinAllowedDirectories": ( - rpc.PermissionsPathsApi.is_path_within_allowed_directories - ), - "session.permissions.paths.isPathWithinWorkspace": ( - rpc.PermissionsPathsApi.is_path_within_workspace - ), - "session.permissions.locations.resolve": rpc.PermissionsLocationsApi.resolve, - "session.permissions.locations.apply": rpc.PermissionsLocationsApi.apply, - "session.permissions.locations.addToolApproval": ( - rpc.PermissionsLocationsApi.add_tool_approval - ), - "session.permissions.folderTrust.isTrusted": rpc.PermissionsFolderTrustApi.is_trusted, - "session.permissions.folderTrust.addTrusted": rpc.PermissionsFolderTrustApi.add_trusted, - "session.permissions.urls.setUnrestrictedMode": (rpc.PermissionsUrlsApi.set_unrestricted_mode), - "session.metadata.getClientMetadata": rpc.MetadataApi.get_client_metadata, - "session.metadata.updateClientMetadata": rpc.MetadataApi.update_client_metadata, - "session.contentExclusion.checkPaths": rpc.ContentExclusionApi.check_paths, - "session.history.clearContext": rpc.HistoryApi.clear_context, - "session.queue.moveItem": rpc.QueueApi.move_item, - "session.queue.insertAt": rpc.QueueApi.insert_at, - "session.queue.removeAt": rpc.QueueApi.remove_at, - "session.queue.updateText": rpc.QueueApi.update_text, - "session.queue.duplicateAt": rpc.QueueApi.duplicate_at, - "session.queue.setDrainPaused": rpc.QueueApi.set_drain_paused, - "session.queue.sendNow": rpc.QueueApi.send_now, - "session.limitPrediction.predict": rpc.LimitPredictionApi.predict, -} - -_FAKE_CLI = r""" -const fs = require("fs"); - -function argValue(name) { - const index = process.argv.indexOf(name); - return index >= 0 ? process.argv[index + 1] : undefined; -} - -const captureFile = argValue("--capture-file"); -const responses = JSON.parse(fs.readFileSync(argValue("--responses-file"), "utf8")); -const requests = []; -let objective = null; -let buffer = Buffer.alloc(0); - -function saveCapture() { - fs.writeFileSync(captureFile, JSON.stringify({ requests })); -} - -function writeResponse(id, result) { - const body = JSON.stringify({ jsonrpc: "2.0", id, result }); - process.stdout.write( - `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}` - ); -} - -function writeError(id, code, message, data) { - const body = JSON.stringify({ jsonrpc: "2.0", id, error: { code, message, data } }); - process.stdout.write( - `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}` - ); -} - -function handleMessage(message) { - if (!Object.prototype.hasOwnProperty.call(message, "id")) return; - - requests.push({ method: message.method, params: message.params }); - saveCapture(); - - if (message.method === "connect") { - writeResponse(message.id, { ok: true, protocolVersion: 3, version: "fake" }); - return; - } - if (message.method === "ping") { - writeResponse(message.id, { - message: "pong", - protocolVersion: 3, - timestamp: 1770000000000, - }); - return; - } - if (message.method === "catalog.search" && message.params.query === "raise-jsonrpc-error") { - writeError(message.id, -32077, "deterministic catalog failure", { - retryable: false, - source: "fake-cli", - }); - return; - } - if (message.method === "session.workspaces.writeAutopilotObjective") { - const operation = objective === null ? "created" : "updated"; - objective = message.params.content; - writeResponse(message.id, { operation }); - return; - } - if (message.method === "session.workspaces.readAutopilotObjective") { - writeResponse(message.id, { content: objective }); - return; - } - if (message.method === "session.workspaces.autopilotObjectiveExists") { - writeResponse(message.id, { exists: objective !== null }); - return; - } - if (message.method === "session.workspaces.deleteAutopilotObjective") { - const deleted = objective !== null; - objective = null; - writeResponse(message.id, { deleted }); - return; - } - - writeResponse(message.id, responses[message.method] ?? {}); -} - -function processBuffer() { - while (true) { - const headerEnd = buffer.indexOf("\r\n\r\n"); - if (headerEnd < 0) return; - const header = buffer.subarray(0, headerEnd).toString("utf8"); - const match = /Content-Length:\s*(\d+)/i.exec(header); - if (!match) throw new Error("Missing Content-Length header"); - const length = Number(match[1]); - const bodyStart = headerEnd + 4; - const bodyEnd = bodyStart + length; - if (buffer.length < bodyEnd) return; - const body = buffer.subarray(bodyStart, bodyEnd).toString("utf8"); - buffer = buffer.subarray(bodyEnd); - handleMessage(JSON.parse(body)); - } -} - -saveCapture(); -process.stdin.on("data", chunk => { - buffer = Buffer.concat([buffer, chunk]); - processBuffer(); -}); -process.stdin.resume(); -""" - - -def _type_globals() -> dict[str, object]: - return { - **vars(generated_session_events), - **vars(rpc), - } - - -def _sample_value( - annotation: object, - *, - depth: int = 0, - stack: tuple[object, ...] = (), -) -> object: - if annotation in (inspect.Signature.empty, typing.Any, object): - return {"sample": "value"} - if annotation in (None, type(None)): - return None - - origin = typing.get_origin(annotation) - arguments = typing.get_args(annotation) - if origin in (typing.Union, types.UnionType): - choices = [item for item in arguments if item is not type(None)] - return _sample_value(choices[0] if choices else type(None), depth=depth, stack=stack) - if origin is typing.Literal: - return arguments[0] - if origin is list: - return [_sample_value(arguments[0], depth=depth + 1, stack=stack)] - if origin is dict: - return {"key": _sample_value(arguments[1], depth=depth + 1, stack=stack)} - if origin is tuple: - return tuple( - _sample_value(item, depth=depth + 1, stack=stack) - for item in arguments - if item is not Ellipsis - ) - if origin is typing.Annotated: - return _sample_value(arguments[0], depth=depth, stack=stack) - - if isinstance(annotation, type) and issubclass(annotation, enum.Enum): - return next(iter(annotation)) - if annotation is str: - return "sample-value" - if annotation is bool: - return True - if annotation is int: - return 7 - if annotation is float: - return 1.5 - if annotation is datetime: - return datetime(2026, 1, 2, 3, 4, 5, tzinfo=UTC) - if annotation is UUID: - return _SAMPLE_UUID - if annotation is list: - return [{"sample": "value"}] - if annotation is dict: - return {"key": "value"} - - if dataclasses.is_dataclass(annotation): - if annotation in stack: - return None - hints = typing.get_type_hints( - annotation, - globalns=_type_globals(), - localns=_type_globals(), - ) - values = {} - for field in dataclasses.fields(annotation): - required = ( - field.default is dataclasses.MISSING - and field.default_factory is dataclasses.MISSING - ) - if required or depth < 4: - values[field.name] = _sample_value( - hints.get(field.name, field.type), - depth=depth + 1, - stack=(*stack, annotation), - ) - constructor = typing.cast(typing.Callable[..., object], annotation) - return constructor(**values) - - return {"sample": "value"} - - -def _method_hints(method: object) -> dict[str, object]: - return typing.get_type_hints( - method, - globalns=_type_globals(), - localns=_type_globals(), - ) - - -def _request_for(method: object) -> typing.Any: - return _sample_value(_method_hints(method)["params"]) - - -def _json_value(value: object) -> object: - if hasattr(value, "to_dict"): - serializer = typing.cast(typing.Callable[[], object], getattr(value, "to_dict")) - return serializer() - if isinstance(value, enum.Enum): - return value.value - if isinstance(value, datetime): - return value.isoformat() - if isinstance(value, UUID): - return str(value) - if isinstance(value, list): - return [_json_value(item) for item in value] - if isinstance(value, dict): - return {key: _json_value(item) for key, item in value.items()} - return value - - -def _response_payloads() -> dict[str, object]: - return { - rpc_method: _json_value(_sample_value(_method_hints(method)["return"])) - for rpc_method, method in _GAP_METHODS.items() - } - - -def _assert_result_matches_payload(result: object, payload: object) -> None: - assert _json_value(result) == payload - - -def _expected_params(method: object, *, session_scoped: bool) -> dict[str, object]: - hints = _method_hints(method) - params = _request_for(method).to_dict() if "params" in hints else {} - if session_scoped: - params["sessionId"] = _SESSION_ID - return params - - -def _assert_request_serialization(capture_path: Path) -> None: - capture = json.loads(capture_path.read_text(encoding="utf-8")) - requests = capture["requests"] - - for rpc_method, method in _GAP_METHODS.items(): - matching = [request for request in requests if request["method"] == rpc_method] - assert matching, f"Missing captured request for {rpc_method}" - expected = _expected_params(method, session_scoped=rpc_method.startswith("session.")) - if rpc_method == "session.workspaces.writeAutopilotObjective": - expected["content"] = "# Deterministic objective\n\nCover generated RPC methods." - assert expected in [request["params"] for request in matching] - - catalog_error = next( - request - for request in requests - if request["method"] == "catalog.search" - and request["params"]["query"] == "raise-jsonrpc-error" - ) - assert catalog_error["params"]["query"] == "raise-jsonrpc-error" - - -async def test_generated_rpc_gap_methods_round_trip_over_fake_cli( - ctx: E2ETestContext, -) -> None: - work_dir = Path(ctx.work_dir) - suffix = str(os.getpid()) - cli_path = work_dir / f"generated-rpc-fake-cli-{suffix}.js" - capture_path = work_dir / f"generated-rpc-capture-{suffix}.json" - responses_path = work_dir / f"generated-rpc-responses-{suffix}.json" - cli_path.write_text(_FAKE_CLI, encoding="utf-8") - responses = _response_payloads() - responses_path.write_text(json.dumps(responses), encoding="utf-8") - - client = CopilotClient( - connection=RuntimeConnection.for_stdio( - path=str(cli_path), - args=[ - "--capture-file", - str(capture_path), - "--responses-file", - str(responses_path), - ], - ), - working_directory=ctx.work_dir, - env=ctx.get_env(), - use_logged_in_user=False, - ) - results: dict[str, object] = {} - - try: - await client.start() - assert client._client is not None - session = CopilotSession(_SESSION_ID, client._client) - - results[ - "registerExtensionLaunchProvider" - ] = await client.rpc.register_extension_launch_provider() - results["hooks.discover"] = await client.rpc.hooks.discover( - _request_for(rpc.ServerHooksApi.discover) - ) - results["models.getBuiltInCatalog"] = await client.rpc.models.get_built_in_catalog() - results["mcp.planInstall"] = await client.rpc.mcp.plan_install( - _request_for(rpc.ServerMcpApi.plan_install) - ) - results["extensions.discover"] = await client.rpc.extensions.discover() - results["extensions.enable"] = await client.rpc.extensions.enable( - _request_for(rpc.ServerExtensionsApi.enable) - ) - results["extensions.disable"] = await client.rpc.extensions.disable( - _request_for(rpc.ServerExtensionsApi.disable) - ) - results["catalog.search"] = await client.rpc.catalog.search( - _request_for(rpc.ServerCatalogApi.search) - ) - results["plugins.builtin.set"] = await client.rpc.plugins.builtin.set( - _request_for(rpc.ServerPluginsBuiltinApi.set) - ) - results[ - "skills.config.setSkillDisabled" - ] = await client.rpc.skills.config.set_skill_disabled( - _request_for(rpc.ServerSkillsConfigApi.set_skill_disabled) - ) - results["commands.list"] = await client.rpc.commands.list() - results["managedSettings.read"] = await client.rpc.managed_settings.read() - results["llmInference.setProvider"] = await client.rpc.llm_inference.set_provider() - results["sessions.getClientMetadata"] = await client.rpc.sessions.get_client_metadata( - _request_for(rpc.ServerSessionsApi.get_client_metadata) - ) - results["sessions.readPersistedEvents"] = await client.rpc.sessions.read_persisted_events( - _request_for(rpc.ServerSessionsApi.read_persisted_events) - ) - - results["session.send"] = await session.rpc.send(_request_for(rpc.SessionRpc.send)) - results["session.sendMessages"] = await session.rpc.send_messages( - _request_for(rpc.SessionRpc.send_messages) - ) - results["session.abort"] = await session.rpc.abort(_request_for(rpc.SessionRpc.abort)) - results["session.interruptMainTurn"] = await session.rpc.interrupt_main_turn( - _request_for(rpc.SessionRpc.interrupt_main_turn) - ) - results[ - "session.cancelAllBackgroundAgents" - ] = await session.rpc.cancel_all_background_agents() - results["session.log"] = await session.rpc.log(_request_for(rpc.SessionRpc.log)) - results[ - "session.sandbox.getEnforcementStatus" - ] = await session.rpc.sandbox.get_enforcement_status() - results[ - "session.sandbox.disableForSession" - ] = await session.rpc.sandbox.disable_for_session( - _request_for(rpc.SandboxApi.disable_for_session) - ) - results["session.debug.collectLogs"] = await session.rpc.debug.collect_logs( - _request_for(rpc.DebugApi.collect_logs) - ) - - results["session.workflow.run"] = await session.rpc.workflow.run( - _request_for(rpc.WorkflowApi.run) - ) - results["session.workflow.resume"] = await session.rpc.workflow.resume( - _request_for(rpc.WorkflowApi.resume) - ) - results["session.workflow.getRun"] = await session.rpc.workflow.get_run( - _request_for(rpc.WorkflowApi.get_run) - ) - results["session.workflow.listRuns"] = await session.rpc.workflow.list_runs( - _request_for(rpc.WorkflowApi.list_runs) - ) - results["session.workflow.getRunDetail"] = await session.rpc.workflow.get_run_detail( - _request_for(rpc.WorkflowApi.get_run_detail) - ) - results["session.workflow.getRunProgress"] = await session.rpc.workflow.get_run_progress( - _request_for(rpc.WorkflowApi.get_run_progress) - ) - results["session.workflow.cancel"] = await session.rpc.workflow.cancel( - _request_for(rpc.WorkflowApi.cancel) - ) - results["session.workflow.pause"] = await session.rpc.workflow.pause( - _request_for(rpc.WorkflowApi.pause) - ) - results["session.workflow.log"] = await session.rpc.workflow.log( - _request_for(rpc.WorkflowApi.log) - ) - results["session.workflow.agent"] = await session.rpc.workflow.agent( - _request_for(rpc.WorkflowApi.agent) - ) - results["session.workflow.journal.get"] = await session.rpc.workflow.journal.get( - _request_for(rpc.WorkflowJournalApi.get) - ) - results["session.workflow.journal.put"] = await session.rpc.workflow.journal.put( - _request_for(rpc.WorkflowJournalApi.put) - ) - - results["session.model.switchAutoTier"] = await session.rpc.model.switch_auto_tier( - _request_for(rpc.ModelApi.switch_auto_tier) - ) - results["session.model.setAllowedModels"] = await session.rpc.model.set_allowed_models( - _request_for(rpc.ModelApi.set_allowed_models) - ) - - results["session.workspaces.updateMetadata"] = await session.rpc.workspaces.update_metadata( - _request_for(rpc.WorkspacesApi.update_metadata) - ) - results["session.workspaces.ensure"] = await session.rpc.workspaces.ensure( - _request_for(rpc.WorkspacesApi.ensure) - ) - results["session.workspaces.statFile"] = await session.rpc.workspaces.stat_file( - _request_for(rpc.WorkspacesApi.stat_file) - ) - results[ - "session.workspaces.createDirectory" - ] = await session.rpc.workspaces.create_directory( - _request_for(rpc.WorkspacesApi.create_directory) - ) - results["session.workspaces.removePath"] = await session.rpc.workspaces.remove_path( - _request_for(rpc.WorkspacesApi.remove_path) - ) - results["session.workspaces.renamePath"] = await session.rpc.workspaces.rename_path( - _request_for(rpc.WorkspacesApi.rename_path) - ) - results["session.workspaces.addSummary"] = await session.rpc.workspaces.add_summary( - _request_for(rpc.WorkspacesApi.add_summary) - ) - results[ - "session.workspaces.truncateSummaries" - ] = await session.rpc.workspaces.truncate_summaries( - _request_for(rpc.WorkspacesApi.truncate_summaries) - ) - - initial_objective = await session.rpc.workspaces.read_autopilot_objective() - assert initial_objective.content is None - initial_exists = await session.rpc.workspaces.autopilot_objective_exists() - assert initial_exists.exists is False - - objective_request = _request_for(rpc.WorkspacesApi.write_autopilot_objective) - objective_request.content = "# Deterministic objective\n\nCover generated RPC methods." - results[ - "session.workspaces.writeAutopilotObjective" - ] = await session.rpc.workspaces.write_autopilot_objective(objective_request) - assert results["session.workspaces.writeAutopilotObjective"].operation == "created" - - saved_objective = await session.rpc.workspaces.read_autopilot_objective() - assert saved_objective.content == objective_request.content - saved_exists = await session.rpc.workspaces.autopilot_objective_exists() - assert saved_exists.exists is True - results["session.workspaces.readAutopilotObjective"] = saved_objective - results["session.workspaces.autopilotObjectiveExists"] = saved_exists - - results[ - "session.workspaces.deleteAutopilotObjective" - ] = await session.rpc.workspaces.delete_autopilot_objective() - assert results["session.workspaces.deleteAutopilotObjective"].deleted is True - assert (await session.rpc.workspaces.read_autopilot_objective()).content is None - - results[ - "session.autopilotObjective.getState" - ] = await session.rpc.autopilot_objective.get_state() - results["session.agent.setPrompt"] = await session.rpc.agent.set_prompt( - _request_for(rpc.AgentApi.set_prompt) - ) - results["session.tasks.register"] = await session.rpc.tasks.register( - _request_for(rpc.TasksApi.register) - ) - results["session.tasks.update"] = await session.rpc.tasks.update( - _request_for(rpc.TasksApi.update) - ) - - results[ - "session.mcp.moveLoadingToBackground" - ] = await session.rpc.mcp.move_loading_to_background() - results["session.mcp.startServer"] = await session.rpc.mcp.start_server( - _request_for(rpc.McpApi.start_server) - ) - results["session.mcp.restartServer"] = await session.rpc.mcp.restart_server( - _request_for(rpc.McpApi.restart_server) - ) - results[ - "session.mcp.oauth.authenticationStateChanged" - ] = await session.rpc.mcp.oauth.authentication_state_changed( - _request_for(rpc.McpOauthApi.authentication_state_changed) - ) - results["session.mcp.oauth.probe"] = await session.rpc.mcp.oauth.probe( - _request_for(rpc.McpOauthApi.probe) - ) - results["session.mcp.oauth.respond"] = await session.rpc.mcp.oauth.respond( - _request_for(rpc.McpOauthApi.respond) - ) - results["session.mcp.resources.read"] = await session.rpc.mcp.resources.read( - _request_for(rpc.McpResourcesApi.read) - ) - results["session.mcp.resources.list"] = await session.rpc.mcp.resources.list( - _request_for(rpc.McpResourcesApi.list) - ) - results[ - "session.mcp.resources.listTemplates" - ] = await session.rpc.mcp.resources.list_templates( - _request_for(rpc.McpResourcesApi.list_templates) - ) - - results["session.tools.execute"] = await session.rpc.tools.execute( - _request_for(rpc.ToolsApi.execute) - ) - results[ - "session.tools.getBuiltinDescriptors" - ] = await session.rpc.tools.get_builtin_descriptors( - _request_for(rpc.ToolsApi.get_builtin_descriptors) - ) - results[ - "session.tools.taskCompleteEventData" - ] = await session.rpc.tools.task_complete_event_data( - _request_for(rpc.ToolsApi.task_complete_event_data) - ) - results["session.tools.set"] = await session.rpc.tools.set(_request_for(rpc.ToolsApi.set)) - - results["session.permissions.configure"] = await session.rpc.permissions.configure( - _request_for(rpc.PermissionsApi.configure) - ) - results[ - "session.permissions.pendingRequests" - ] = await session.rpc.permissions.pending_requests() - results["session.permissions.modifyRules"] = await session.rpc.permissions.modify_rules( - _request_for(rpc.PermissionsApi.modify_rules) - ) - results["session.permissions.setRequired"] = await session.rpc.permissions.set_required( - _request_for(rpc.PermissionsApi.set_required) - ) - results[ - "session.permissions.notifyPromptShown" - ] = await session.rpc.permissions.notify_prompt_shown( - _request_for(rpc.PermissionsApi.notify_prompt_shown) - ) - results["session.permissions.paths.list"] = await session.rpc.permissions.paths.list() - results["session.permissions.paths.add"] = await session.rpc.permissions.paths.add( - _request_for(rpc.PermissionsPathsApi.add) - ) - results[ - "session.permissions.paths.updatePrimary" - ] = await session.rpc.permissions.paths.update_primary( - _request_for(rpc.PermissionsPathsApi.update_primary) - ) - results[ - "session.permissions.paths.isPathWithinAllowedDirectories" - ] = await session.rpc.permissions.paths.is_path_within_allowed_directories( - _request_for(rpc.PermissionsPathsApi.is_path_within_allowed_directories) - ) - results[ - "session.permissions.paths.isPathWithinWorkspace" - ] = await session.rpc.permissions.paths.is_path_within_workspace( - _request_for(rpc.PermissionsPathsApi.is_path_within_workspace) - ) - results[ - "session.permissions.locations.resolve" - ] = await session.rpc.permissions.locations.resolve( - _request_for(rpc.PermissionsLocationsApi.resolve) - ) - results[ - "session.permissions.locations.apply" - ] = await session.rpc.permissions.locations.apply( - _request_for(rpc.PermissionsLocationsApi.apply) - ) - results[ - "session.permissions.locations.addToolApproval" - ] = await session.rpc.permissions.locations.add_tool_approval( - _request_for(rpc.PermissionsLocationsApi.add_tool_approval) - ) - results[ - "session.permissions.folderTrust.isTrusted" - ] = await session.rpc.permissions.folder_trust.is_trusted( - _request_for(rpc.PermissionsFolderTrustApi.is_trusted) - ) - results[ - "session.permissions.folderTrust.addTrusted" - ] = await session.rpc.permissions.folder_trust.add_trusted( - _request_for(rpc.PermissionsFolderTrustApi.add_trusted) - ) - results[ - "session.permissions.urls.setUnrestrictedMode" - ] = await session.rpc.permissions.urls.set_unrestricted_mode( - _request_for(rpc.PermissionsUrlsApi.set_unrestricted_mode) - ) - - results[ - "session.metadata.getClientMetadata" - ] = await session.rpc.metadata.get_client_metadata() - results[ - "session.metadata.updateClientMetadata" - ] = await session.rpc.metadata.update_client_metadata( - _request_for(rpc.MetadataApi.update_client_metadata) - ) - results[ - "session.contentExclusion.checkPaths" - ] = await session.rpc.content_exclusion.check_paths( - _request_for(rpc.ContentExclusionApi.check_paths) - ) - results["session.history.clearContext"] = await session.rpc.history.clear_context( - _request_for(rpc.HistoryApi.clear_context) - ) - - results["session.queue.moveItem"] = await session.rpc.queue.move_item( - _request_for(rpc.QueueApi.move_item) - ) - results["session.queue.insertAt"] = await session.rpc.queue.insert_at( - _request_for(rpc.QueueApi.insert_at) - ) - results["session.queue.removeAt"] = await session.rpc.queue.remove_at( - _request_for(rpc.QueueApi.remove_at) - ) - results["session.queue.updateText"] = await session.rpc.queue.update_text( - _request_for(rpc.QueueApi.update_text) - ) - results["session.queue.duplicateAt"] = await session.rpc.queue.duplicate_at( - _request_for(rpc.QueueApi.duplicate_at) - ) - results["session.queue.setDrainPaused"] = await session.rpc.queue.set_drain_paused( - _request_for(rpc.QueueApi.set_drain_paused) - ) - results["session.queue.sendNow"] = await session.rpc.queue.send_now( - _request_for(rpc.QueueApi.send_now) - ) - results["session.limitPrediction.predict"] = await session.rpc.limit_prediction.predict( - _request_for(rpc.LimitPredictionApi.predict) - ) - - category_requests = [ - rpc.CatalogSearchRequest( - contract=rpc.CatalogClientContract( - protocol_version=3, - required_capabilities=["catalog-search"], - ), - query="all candidates", - kinds=None, - limit=5, - ), - rpc.CatalogSearchRequest( - contract=rpc.CatalogClientContract( - protocol_version=3, - required_capabilities=["catalog-search"], - ), - query="MCP candidates", - kinds=[rpc.CatalogCandidateKind.MCP_SERVER], - limit=5, - ), - rpc.CatalogSearchRequest( - contract=rpc.CatalogClientContract( - protocol_version=3, - required_capabilities=["catalog-search"], - ), - query="skill candidates", - kinds=[rpc.CatalogCandidateKind.AI_SKILL], - limit=5, - ), - ] - for category_request in category_requests: - category_result = await client.rpc.catalog.search(category_request) - assert isinstance(category_result, rpc.CatalogSearchSucceeded) - assert category_result.search_id == "sample-value" - - error_request = _request_for(rpc.ServerCatalogApi.search) - error_request.query = "raise-jsonrpc-error" - with pytest.raises(JsonRpcError) as exc_info: - await client.rpc.catalog.search(error_request) - assert exc_info.value.code == -32077 - assert exc_info.value.message == "deterministic catalog failure" - assert exc_info.value.data == {"retryable": False, "source": "fake-cli"} - - for rpc_method, payload in responses.items(): - if rpc_method not in _OBJECTIVE_METHODS: - _assert_result_matches_payload(results[rpc_method], payload) - - planned = results["mcp.planInstall"] - assert isinstance(planned, rpc.MCPPlanInstallPlanned) - assert planned.plan.transport_choices[0].transport - assert planned.plan.transport_choices[0].required_values[0].key == "sample-value" - - catalog = results["catalog.search"] - assert isinstance(catalog, rpc.CatalogSearchSucceeded) - assert catalog.candidates[0].installability.value - assert catalog.candidates[0].provenance.authority == "sample-value" - - workflow_detail = results["session.workflow.getRunDetail"] - assert workflow_detail.consumed.active_ms == 7 - assert workflow_detail.agents[0].agent_id == "sample-value" - assert workflow_detail.progress.records[0].seq == 7 - - permission_requests = results["session.permissions.pendingRequests"] - assert len(permission_requests.items) == 1 - assert permission_requests.items[0].request_id == "sample-value" - assert isinstance( - permission_requests.items[0].request, - generated_session_events.PermissionPromptRequestCommands, - ) - assert permission_requests.items[0].request.kind == "commands" - assert permission_requests.items[0].request.command_identifiers == ["sample-value"] - - mcp_resources = results["session.mcp.resources.read"] - assert mcp_resources.contents[0].uri == "sample-value" - assert mcp_resources.contents[0].mime_type == "sample-value" - - tool_result = typing.cast(dict[str, typing.Any], results["session.tools.execute"]) - assert tool_result["resultType"] == "denied" - assert tool_result["binaryResultsForLlm"][0]["metadata"]["key"]["sample"] == "value" - assert tool_result["taskCompletionDecision"]["reviewerResultMeta"]["sample"] == "value" - - content_checks = results["session.contentExclusion.checkPaths"] - assert content_checks.available is True - assert content_checks.checks[0].excluded is True - - _assert_request_serialization(capture_path) - captured_catalog_params = [ - request["params"] - for request in json.loads(capture_path.read_text(encoding="utf-8"))["requests"] - if request["method"] == "catalog.search" - ] - for category_request in category_requests: - assert category_request.to_dict() in captured_catalog_params - finally: - await client.force_stop() - cli_path.unlink(missing_ok=True) - capture_path.unlink(missing_ok=True) - responses_path.unlink(missing_ok=True) diff --git a/python/e2e/test_rpc_mcp_and_skills_e2e.py b/python/e2e/test_rpc_mcp_and_skills_e2e.py deleted file mode 100644 index 14231dbbd1..0000000000 --- a/python/e2e/test_rpc_mcp_and_skills_e2e.py +++ /dev/null @@ -1,435 +0,0 @@ -""" -E2E coverage for session-scoped MCP, skills, plugins, and extensions RPCs. - -Mirrors ``dotnet/test/RpcMcpAndSkillsTests.cs`` (snapshot category -``rpc_mcp_and_skills``). -""" - -from __future__ import annotations - -import asyncio -import os -import time -import uuid -from pathlib import Path - -import pytest -import pytest_asyncio - -from copilot.rpc import ( - ExtensionsDisableRequest, - ExtensionsEnableRequest, - MCPAppsCallToolRequest, - MCPAppsDiagnoseRequest, - MCPAppsDisplayMode, - MCPAppsHostContextDetailsPlatform, - MCPAppsListToolsRequest, - MCPAppsReadResourceRequest, - MCPAppsSetHostContextDetails, - MCPAppsSetHostContextRequest, - MCPCancelSamplingExecutionParams, - MCPDisableRequest, - MCPEnableRequest, - MCPExecuteSamplingParams, - MCPRemoveGitHubResult, - MCPSamplingExecutionAction, - MCPSetEnvValueModeDetails, - MCPSetEnvValueModeParams, - SkillsDisableRequest, - SkillsEnableRequest, - Theme, -) -from copilot.session import PermissionHandler -from copilot.session_events import McpServerStatus - -from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - -TEST_MCP_SERVER = str( - (Path(__file__).parents[2] / "test" / "harness" / "test-mcp-server.mjs").resolve() -) -TEST_HARNESS_DIR = str((Path(__file__).parents[2] / "test" / "harness").resolve()) - - -# --yolo auto-approves extension permission gates at the CLI level, -# preventing breakage from new gates (e.g., extension-permission-access). -@pytest_asyncio.fixture(scope="module", loop_scope="module") -async def ctx(request): - """Module-scoped context with --yolo for extension test hardening.""" - context = E2ETestContext() - await context.setup(cli_args=["--yolo"]) - yield context - any_failed = request.session.stash.get("any_test_failed", False) - await context.teardown(test_failed=any_failed) - - -def _create_skill(skills_dir: Path, skill_name: str, description: str) -> None: - skill_subdir = skills_dir / skill_name - skill_subdir.mkdir(parents=True, exist_ok=True) - skill_md = ( - f"---\n" - f"name: {skill_name}\n" - f"description: {description}\n" - f"---\n\n" - f"# {skill_name}\n\n" - f"This skill is used by RPC E2E tests.\n" - ) - (skill_subdir / "SKILL.md").write_text(skill_md, encoding="utf-8", newline="\n") - - -def _create_skill_directory(work_dir: str, skill_name: str, description: str) -> str: - skills_dir = Path(work_dir) / "session-rpc-skills" / uuid.uuid4().hex - skills_dir.mkdir(parents=True, exist_ok=True) - _create_skill(skills_dir, skill_name, description) - return str(skills_dir) - - -def _test_mcp_servers(*server_names: str) -> dict: - return { - server_name: { - "command": "node", - "args": [TEST_MCP_SERVER], - "tools": ["*"], - "working_directory": TEST_HARNESS_DIR, - } - for server_name in server_names - } - - -async def _wait_for_mcp_server_status( - session, server_name: str, expected_status: McpServerStatus = McpServerStatus.CONNECTED -) -> None: - deadline = time.monotonic() + 60 - last_status = "" - - while time.monotonic() < deadline: - result = await session.rpc.mcp.list() - server = next((s for s in result.servers if s.name == server_name), None) - if server is not None and server.status == expected_status: - return - last_status = server.status if server is not None else "" - await asyncio.sleep(0.2) - - raise AssertionError( - f"{server_name} did not reach {expected_status.value}; last status was {last_status}" - ) - - -def _assert_skill(skills, skill_name: str, *, enabled: bool): - matching = [s for s in skills if s.name == skill_name] - assert len(matching) == 1, f"Expected exactly one skill named {skill_name!r}" - skill = matching[0] - assert skill.enabled is enabled - assert skill.path is not None - assert skill.path.endswith(os.path.join(skill_name, "SKILL.md")) - return skill - - -async def _assert_failure(awaitable, expected: str) -> None: - with pytest.raises(Exception) as excinfo: - _ = await awaitable - assert expected.lower() in str(excinfo.value).lower() - - -async def _assert_implemented_failure(awaitable, method: str) -> None: - with pytest.raises(Exception) as excinfo: - _ = await awaitable - assert f"unhandled method {method}".lower() not in str(excinfo.value).lower() - - -class TestRpcMcpAndSkills: - async def test_should_list_and_toggle_session_skills(self, ctx: E2ETestContext): - skill_name = f"session-rpc-skill-{uuid.uuid4().hex}" - skills_dir = _create_skill_directory( - ctx.work_dir, skill_name, "Session skill controlled by RPC." - ) - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - skill_directories=[skills_dir], - disabled_skills=[skill_name], - ) - try: - disabled = await session.rpc.skills.list() - _assert_skill(disabled.skills, skill_name, enabled=False) - - await session.rpc.skills.enable(SkillsEnableRequest(name=skill_name)) - enabled = await session.rpc.skills.list() - _assert_skill(enabled.skills, skill_name, enabled=True) - - await session.rpc.skills.disable(SkillsDisableRequest(name=skill_name)) - disabled_again = await session.rpc.skills.list() - _assert_skill(disabled_again.skills, skill_name, enabled=False) - finally: - await session.disconnect() - - async def test_should_reload_session_skills(self, ctx: E2ETestContext): - skills_dir = Path(ctx.work_dir) / "reloadable-rpc-skills" / uuid.uuid4().hex - skills_dir.mkdir(parents=True, exist_ok=True) - skill_name = f"reload-rpc-skill-{uuid.uuid4().hex}" - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - skill_directories=[str(skills_dir)], - ) - try: - before = await session.rpc.skills.list() - assert all(s.name != skill_name for s in before.skills) - - _create_skill(skills_dir, skill_name, "Skill added after session creation.") - await session.rpc.skills.reload() - - after = await session.rpc.skills.list() - reloaded = _assert_skill(after.skills, skill_name, enabled=True) - assert reloaded.description == "Skill added after session creation." - finally: - await session.disconnect() - - async def test_should_ensure_skills_loaded_and_report_no_invoked_skills_for_fresh_session( - self, ctx: E2ETestContext - ): - skill_name = f"ensure-rpc-skill-{uuid.uuid4().hex}" - skills_dir = _create_skill_directory( - ctx.work_dir, skill_name, "Skill loaded explicitly by RPC." - ) - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - skill_directories=[skills_dir], - ) - try: - await session.rpc.skills.ensure_loaded() - listed = await session.rpc.skills.list() - _assert_skill(listed.skills, skill_name, enabled=True) - - invoked = await session.rpc.skills.get_invoked() - assert invoked.skills == [] - finally: - await session.disconnect() - - async def test_should_list_mcp_servers_with_configured_server(self, ctx: E2ETestContext): - server_name = "rpc-list-mcp-server" - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - mcp_servers=_test_mcp_servers(server_name), - ) - try: - await _wait_for_mcp_server_status(session, server_name) - result = await session.rpc.mcp.list() - matching = [s for s in result.servers if s.name == server_name] - assert len(matching) == 1 - assert matching[0].status is not None - finally: - await session.disconnect() - - async def test_should_list_plugins(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - result = await session.rpc.plugins.list() - assert result.plugins is not None - assert all((p.name or "").strip() for p in result.plugins) - finally: - await session.disconnect() - - async def test_should_list_extensions(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - result = await session.rpc.extensions.list() - assert result.extensions is not None - for extension in result.extensions: - assert (extension.id or "").strip() - assert (extension.name or "").strip() - finally: - await session.disconnect() - - async def test_should_report_error_when_mcp_host_is_not_initialized(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - await _assert_failure( - session.rpc.mcp.enable(MCPEnableRequest(server_name="missing-server")), - "No MCP host initialized", - ) - await _assert_failure( - session.rpc.mcp.disable(MCPDisableRequest(server_name="missing-server")), - "No MCP host initialized", - ) - await _assert_failure( - session.rpc.mcp.reload(), - "MCP config reload not available", - ) - finally: - await session.disconnect() - - async def test_should_report_error_when_extensions_are_not_available(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - await _assert_failure( - session.rpc.extensions.enable(ExtensionsEnableRequest(id="missing-extension")), - "Extensions not available", - ) - await _assert_failure( - session.rpc.extensions.disable(ExtensionsDisableRequest(id="missing-extension")), - "Extensions not available", - ) - await _assert_failure( - session.rpc.extensions.reload(), - "Extensions not available", - ) - finally: - await session.disconnect() - - async def test_should_set_mcp_env_mode_remove_github_and_cancel_missing_sampling( - self, ctx: E2ETestContext - ): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - indirect = await session.rpc.mcp.set_env_value_mode( - MCPSetEnvValueModeParams(mode=MCPSetEnvValueModeDetails.INDIRECT) - ) - assert indirect.mode == MCPSetEnvValueModeDetails.INDIRECT - - direct = await session.rpc.mcp.set_env_value_mode( - MCPSetEnvValueModeParams(mode=MCPSetEnvValueModeDetails.DIRECT) - ) - assert direct.mode == MCPSetEnvValueModeDetails.DIRECT - - removed = await session.rpc.mcp.remove_git_hub() - assert isinstance(removed, MCPRemoveGitHubResult) - assert removed.removed in (True, False) - - cancelled = await session.rpc.mcp.cancel_sampling_execution( - MCPCancelSamplingExecutionParams(request_id="missing-sampling-request") - ) - assert cancelled.cancelled is False - finally: - await session.disconnect() - - async def test_should_report_failure_or_implemented_error_for_missing_mcp_sampling( - self, ctx: E2ETestContext - ): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - try: - result = await session.rpc.mcp.execute_sampling( - MCPExecuteSamplingParams( - mcp_request_id="mcp-sampling-e2e", - request={ - "messages": [ - { - "role": "user", - "content": {"type": "text", "text": "hello"}, - } - ], - "maxTokens": 16, - }, - request_id=f"sampling-{uuid.uuid4().hex}", - server_name="missing-server", - ) - ) - except Exception as exc: - assert "unhandled method session.mcp.executesampling" not in str(exc).lower() - else: - assert result.action == MCPSamplingExecutionAction.FAILURE - assert result.error - finally: - await session.disconnect() - - async def test_should_round_trip_mcp_apps_host_context_and_diagnose_shape( - self, ctx: E2ETestContext - ): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - await session.rpc.mcp.apps.set_host_context( - MCPAppsSetHostContextRequest( - context=MCPAppsSetHostContextDetails( - available_display_modes=[ - MCPAppsDisplayMode.INLINE, - MCPAppsDisplayMode.FULLSCREEN, - ], - display_mode=MCPAppsDisplayMode.INLINE, - locale="en-US", - platform=MCPAppsHostContextDetailsPlatform.DESKTOP, - theme=Theme.DARK, - time_zone="Etc/UTC", - user_agent="python-sdk-e2e", - ) - ) - ) - - host_context = await session.rpc.mcp.apps.get_host_context() - assert host_context.context.display_mode == MCPAppsDisplayMode.INLINE - assert host_context.context.locale == "en-US" - assert host_context.context.platform == MCPAppsHostContextDetailsPlatform.DESKTOP - assert host_context.context.theme == Theme.DARK - assert host_context.context.time_zone == "Etc/UTC" - assert host_context.context.user_agent == "python-sdk-e2e" - assert MCPAppsDisplayMode.FULLSCREEN in ( - host_context.context.available_display_modes or [] - ) - - diagnose = await session.rpc.mcp.apps.diagnose( - MCPAppsDiagnoseRequest(server_name="missing-mcp-app-server") - ) - assert diagnose.capability.advertised in (True, False) - assert diagnose.capability.feature_flag_enabled in (True, False) - assert diagnose.capability.session_has_mcp_apps in (True, False) - assert diagnose.server.connected is False - assert diagnose.server.tool_count >= 0 - assert diagnose.server.tools_with_ui_meta >= 0 - assert diagnose.server.sample_tool_names is not None - finally: - await session.disconnect() - - async def test_should_report_implemented_errors_for_mcp_apps_without_capability( - self, ctx: E2ETestContext - ): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - await _assert_implemented_failure( - session.rpc.mcp.apps.list_tools( - MCPAppsListToolsRequest( - origin_server_name="missing-server", - server_name="missing-server", - ) - ), - "session.mcp.apps.listTools", - ) - await _assert_implemented_failure( - session.rpc.mcp.apps.call_tool( - MCPAppsCallToolRequest( - origin_server_name="missing-server", - server_name="missing-server", - tool_name="missing-tool", - arguments={}, - ) - ), - "session.mcp.apps.callTool", - ) - await _assert_implemented_failure( - session.rpc.mcp.apps.read_resource( - MCPAppsReadResourceRequest( - server_name="missing-server", - uri="ui://missing/resource.html", - ) - ), - "session.mcp.apps.readResource", - ) - finally: - await session.disconnect() diff --git a/python/e2e/test_rpc_mcp_config_e2e.py b/python/e2e/test_rpc_mcp_config_e2e.py deleted file mode 100644 index db605e07d6..0000000000 --- a/python/e2e/test_rpc_mcp_config_e2e.py +++ /dev/null @@ -1,122 +0,0 @@ -""" -E2E coverage for ``mcp.config.*`` server-scoped RPCs. - -Mirrors ``dotnet/test/RpcMcpConfigTests.cs`` (snapshot category -``rpc_mcp_config``). -""" - -from __future__ import annotations - -import uuid - -import pytest - -from copilot.rpc import ( - MCPConfigAddRequest, - MCPConfigDisableRequest, - MCPConfigEnableRequest, - MCPConfigRemoveRequest, - MCPConfigUpdateRequest, - MCPGrantType, - MCPSerializableServerConfig, - MCPSerializableServerConfigType, -) - -from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -def _server_config(servers: dict, name: str) -> MCPSerializableServerConfig: - assert name in servers, f"Expected MCP server '{name}' to be present." - return servers[name] - - -class TestRpcMcpConfig: - async def test_should_call_server_mcp_config_rpcs(self, ctx: E2ETestContext): - await ctx.client.start() - - server_name = f"sdk-test-{uuid.uuid4().hex}" - config = MCPSerializableServerConfig(command="node", args=[]) - updated_config = MCPSerializableServerConfig(command="node", args=["--version"]) - - initial = await ctx.client.rpc.mcp.config.list() - assert server_name not in initial.servers - - try: - await ctx.client.rpc.mcp.config.add( - MCPConfigAddRequest(name=server_name, config=config) - ) - after_add = await ctx.client.rpc.mcp.config.list() - assert server_name in after_add.servers - - await ctx.client.rpc.mcp.config.update( - MCPConfigUpdateRequest(name=server_name, config=updated_config) - ) - after_update = await ctx.client.rpc.mcp.config.list() - updated = _server_config(after_update.servers, server_name) - assert updated.command == "node" - assert updated.args is not None and updated.args[0] == "--version" - - await ctx.client.rpc.mcp.config.disable(MCPConfigDisableRequest(names=[server_name])) - await ctx.client.rpc.mcp.config.enable(MCPConfigEnableRequest(names=[server_name])) - finally: - await ctx.client.rpc.mcp.config.remove(MCPConfigRemoveRequest(name=server_name)) - - after_remove = await ctx.client.rpc.mcp.config.list() - assert server_name not in after_remove.servers - - async def test_should_round_trip_http_mcp_oauth_config_rpc(self, ctx: E2ETestContext): - await ctx.client.start() - - server_name = f"sdk-http-oauth-{uuid.uuid4().hex}" - config = MCPSerializableServerConfig( - type=MCPSerializableServerConfigType.HTTP, - url="https://example.com/mcp", - headers={"Authorization": "Bearer token"}, - oauth_client_id="client-id", - oauth_public_client=False, - oauth_grant_type=MCPGrantType.CLIENT_CREDENTIALS, - tools=["*"], - timeout=3000, - ) - updated_config = MCPSerializableServerConfig( - type=MCPSerializableServerConfigType.HTTP, - url="https://example.com/updated-mcp", - oauth_client_id="updated-client-id", - oauth_public_client=True, - oauth_grant_type=MCPGrantType.AUTHORIZATION_CODE, - tools=["updated-tool"], - timeout=4000, - ) - - try: - await ctx.client.rpc.mcp.config.add( - MCPConfigAddRequest(name=server_name, config=config) - ) - after_add = await ctx.client.rpc.mcp.config.list() - added = _server_config(after_add.servers, server_name) - assert added.type == MCPSerializableServerConfigType.HTTP - assert added.url == "https://example.com/mcp" - assert added.headers is not None - assert added.headers["Authorization"] == "Bearer token" - assert added.oauth_client_id == "client-id" - assert added.oauth_public_client is False - assert added.oauth_grant_type == MCPGrantType.CLIENT_CREDENTIALS - - await ctx.client.rpc.mcp.config.update( - MCPConfigUpdateRequest(name=server_name, config=updated_config) - ) - after_update = await ctx.client.rpc.mcp.config.list() - updated = _server_config(after_update.servers, server_name) - assert updated.url == "https://example.com/updated-mcp" - assert updated.oauth_client_id == "updated-client-id" - assert updated.oauth_public_client is True - assert updated.oauth_grant_type == MCPGrantType.AUTHORIZATION_CODE - assert updated.tools is not None and updated.tools[0] == "updated-tool" - assert updated.timeout == 4000 - finally: - await ctx.client.rpc.mcp.config.remove(MCPConfigRemoveRequest(name=server_name)) - - after_remove = await ctx.client.rpc.mcp.config.list() - assert server_name not in after_remove.servers diff --git a/python/e2e/test_rpc_mcp_lifecycle_e2e.py b/python/e2e/test_rpc_mcp_lifecycle_e2e.py deleted file mode 100644 index a16603706a..0000000000 --- a/python/e2e/test_rpc_mcp_lifecycle_e2e.py +++ /dev/null @@ -1,151 +0,0 @@ -""" -E2E coverage for session-scoped MCP lifecycle RPC methods. - -Mirrors ``dotnet/test/E2E/RpcMcpLifecycleE2ETests.cs`` (snapshot category -``rpc_mcp_lifecycle``). -""" - -from __future__ import annotations - -import uuid -from pathlib import Path - -import pytest - -from copilot.rpc import ( - MCPIsServerRunningRequest, - MCPListToolsRequest, - MCPStopServerRequest, -) -from copilot.session import PermissionHandler -from copilot.session_events import McpServerStatus - -from .testharness import E2ETestContext, wait_for_condition - -pytestmark = pytest.mark.asyncio(loop_scope="module") - -TEST_MCP_SERVER = str( - (Path(__file__).parents[2] / "test" / "harness" / "test-mcp-server.mjs").resolve() -) -TEST_HARNESS_DIR = str((Path(__file__).parents[2] / "test" / "harness").resolve()) - - -def _test_mcp_servers(*server_names: str) -> dict[str, dict]: - return { - server_name: { - "command": "node", - "args": [TEST_MCP_SERVER], - "tools": ["*"], - "working_directory": TEST_HARNESS_DIR, - } - for server_name in server_names - } - - -async def _wait_for_mcp_server_status( - session, - server_name: str, - expected_status: McpServerStatus = McpServerStatus.CONNECTED, -) -> None: - last_status = "" - - async def connected() -> bool: - nonlocal last_status - result = await session.rpc.mcp.list() - server = next((s for s in result.servers if s.name == server_name), None) - if server is not None: - last_status = server.status - if server is None: - last_status = "" - return False - return server.status == expected_status - - await wait_for_condition( - connected, - timeout=60.0, - poll_interval=0.2, - timeout_message=( - f"{server_name} did not reach {expected_status.value}; last status was {last_status}" - ), - ) - - -async def _wait_for_mcp_running(session, server_name: str, expected_running: bool) -> None: - async def matches() -> bool: - result = await session.rpc.mcp.is_server_running( - MCPIsServerRunningRequest(server_name=server_name) - ) - return result.running is expected_running - - await wait_for_condition( - matches, - timeout=60.0, - poll_interval=0.2, - timeout_message=f"{server_name} running={expected_running}", - ) - - -def _assert_not_unhandled_method(message: str) -> None: - assert "Unhandled method".lower() not in message.lower() - - -class TestRpcMcpLifecycle: - async def test_should_list_tools_and_report_running_status_for_connected_server( - self, ctx: E2ETestContext - ): - server_name = "rpc-lifecycle-list-server" - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - mcp_servers=_test_mcp_servers(server_name), - ) as session: - await _wait_for_mcp_server_status(session, server_name) - - tools = await session.rpc.mcp.list_tools(MCPListToolsRequest(server_name=server_name)) - assert tools.tools is not None - assert len(tools.tools) > 0 - assert all((tool.name or "").strip() for tool in tools.tools) - - running = await session.rpc.mcp.is_server_running( - MCPIsServerRunningRequest(server_name=server_name) - ) - assert running.running is True - - missing = await session.rpc.mcp.is_server_running( - MCPIsServerRunningRequest(server_name=f"missing-{uuid.uuid4().hex}") - ) - assert missing.running is False - - async def test_should_throw_when_listing_tools_for_unconnected_server( - self, ctx: E2ETestContext - ): - server_name = "rpc-lifecycle-unconnected-host" - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - mcp_servers=_test_mcp_servers(server_name), - ) as session: - await _wait_for_mcp_server_status(session, server_name) - - with pytest.raises(Exception) as excinfo: - await session.rpc.mcp.list_tools( - MCPListToolsRequest(server_name=f"missing-{uuid.uuid4().hex}") - ) - message = str(excinfo.value) - _assert_not_unhandled_method(message) - assert "not connected" in message.lower() - - async def test_should_stop_running_mcp_server(self, ctx: E2ETestContext): - server_name = "rpc-lifecycle-stop-server" - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - mcp_servers=_test_mcp_servers(server_name), - ) as session: - await _wait_for_mcp_server_status(session, server_name) - assert ( - await session.rpc.mcp.is_server_running( - MCPIsServerRunningRequest(server_name=server_name) - ) - ).running is True - - await session.rpc.mcp.stop_server(MCPStopServerRequest(server_name=server_name)) - - await _wait_for_mcp_running(session, server_name, expected_running=False) diff --git a/python/e2e/test_rpc_queue_e2e.py b/python/e2e/test_rpc_queue_e2e.py deleted file mode 100644 index edd286aa44..0000000000 --- a/python/e2e/test_rpc_queue_e2e.py +++ /dev/null @@ -1,168 +0,0 @@ -"""E2E coverage for session.queue RPC methods.""" - -from __future__ import annotations - -import asyncio -import time -import uuid - -import pytest - -from copilot.rpc import ( - CommandsRespondToQueuedCommandRequest, - EnqueueCommandParams, - QueuedCommandHandled, - QueuePendingItems, - QueuePendingItemsKind, - RegisterEventInterestParams, - ReleaseEventInterestParams, -) -from copilot.session import PermissionHandler -from copilot.session_events import CommandQueuedData - -from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -def _is_pending_command(item: QueuePendingItems, command: str) -> bool: - return item.kind == QueuePendingItemsKind.COMMAND and ( - item.display_text == command or command.lstrip("/") in item.display_text - ) - - -async def _wait_for_command_in_pending_items(session, command: str) -> QueuePendingItems: - deadline = time.monotonic() + 30.0 - last_items = [] - while time.monotonic() < deadline: - pending = await session.rpc.queue.pending_items() - last_items = pending.items - for item in pending.items: - if _is_pending_command(item, command): - assert item.kind == QueuePendingItemsKind.COMMAND - assert command.lstrip("/") in item.display_text - return item - await asyncio.sleep(0.2) - raise AssertionError(f"Timed out waiting for {command!r} in pending items: {last_items!r}") - - -async def _wait_for_command_not_in_pending_items(session, command: str) -> None: - deadline = time.monotonic() + 30.0 - while time.monotonic() < deadline: - pending = await session.rpc.queue.pending_items() - if not any(_is_pending_command(item, command) for item in pending.items): - return - await asyncio.sleep(0.2) - pytest.fail(f"Timed out waiting for {command!r} to leave pending items.") - - -async def _assert_queue_empty(session) -> None: - pending = await session.rpc.queue.pending_items() - assert pending.items == [] - assert pending.steering_messages == [] - - -class TestRpcQueue: - async def test_fresh_queue_is_empty_and_empty_mutations_are_noops(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - await _assert_queue_empty(session) - - remove = await session.rpc.queue.remove_most_recent() - assert remove.removed is False - await _assert_queue_empty(session) - - await session.rpc.queue.clear() - await _assert_queue_empty(session) - - remove_after_clear = await session.rpc.queue.remove_most_recent() - assert remove_after_clear.removed is False - finally: - await session.disconnect() - - async def test_pending_items_reports_queued_command_and_mutations_update_queue( - self, ctx: E2ETestContext - ): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - interest = None - first_event = None - responded_to_first = False - try: - interest = await session.rpc.event_log.register_interest( - RegisterEventInterestParams(event_type="command.queued") - ) - - first_command = f"/sdk-queue-first-{uuid.uuid4().hex}" - second_command = f"/sdk-queue-second-{uuid.uuid4().hex}" - third_command = f"/sdk-queue-third-{uuid.uuid4().hex}" - first_queued: asyncio.Future = asyncio.get_event_loop().create_future() - - def on_event(event): - if ( - isinstance(event.data, CommandQueuedData) - and event.data.command == first_command - and not first_queued.done() - ): - first_queued.set_result(event) - - unsubscribe = session.on(on_event) - try: - first = await session.rpc.commands.enqueue( - EnqueueCommandParams(command=first_command) - ) - assert first.queued is True - first_event = await asyncio.wait_for(first_queued, timeout=30.0) - finally: - unsubscribe() - - second = await session.rpc.commands.enqueue( - EnqueueCommandParams(command=second_command) - ) - assert second.queued is True - await _wait_for_command_in_pending_items(session, second_command) - - remove = await session.rpc.queue.remove_most_recent() - assert remove.removed is True - await _wait_for_command_not_in_pending_items(session, second_command) - - third = await session.rpc.commands.enqueue(EnqueueCommandParams(command=third_command)) - assert third.queued is True - await _wait_for_command_in_pending_items(session, third_command) - - await session.rpc.queue.clear() - await _wait_for_command_not_in_pending_items(session, third_command) - - completed = await session.rpc.commands.respond_to_queued_command( - CommandsRespondToQueuedCommandRequest( - request_id=first_event.data.request_id, - result=QueuedCommandHandled(stop_processing_queue=True), - ) - ) - responded_to_first = completed.success - assert completed.success is True - - deadline = time.monotonic() + 30.0 - while time.monotonic() < deadline: - pending = await session.rpc.queue.pending_items() - if pending.items == [] and pending.steering_messages == []: - break - await asyncio.sleep(0.2) - await _assert_queue_empty(session) - finally: - if not responded_to_first and first_event is not None: - await session.rpc.commands.respond_to_queued_command( - CommandsRespondToQueuedCommandRequest( - request_id=first_event.data.request_id, - result=QueuedCommandHandled(stop_processing_queue=True), - ) - ) - await session.rpc.queue.clear() - if interest is not None and interest.handle: - await session.rpc.event_log.release_interest( - ReleaseEventInterestParams(handle=interest.handle) - ) - await session.disconnect() diff --git a/python/e2e/test_rpc_remote_e2e.py b/python/e2e/test_rpc_remote_e2e.py deleted file mode 100644 index 0d60c368cf..0000000000 --- a/python/e2e/test_rpc_remote_e2e.py +++ /dev/null @@ -1,86 +0,0 @@ -"""E2E coverage for session.remote RPC methods.""" - -from __future__ import annotations - -import asyncio -import time - -import pytest - -from copilot.rpc import ( - RemoteEnableRequest, - RemoteNotifySteerableChangedRequest, - RemoteSessionMode, -) -from copilot.session import PermissionHandler -from copilot.session_events import SessionRemoteSteerableChangedData - -from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -async def _wait_for_remote_steerable_event(session, expected: bool) -> None: - deadline = time.monotonic() + 30.0 - while time.monotonic() < deadline: - events = await session.get_events() - if any( - isinstance(evt.data, SessionRemoteSteerableChangedData) - and evt.data.remote_steerable is expected - for evt in events - ): - return - await asyncio.sleep(0.2) - pytest.fail(f"Timed out waiting for session.remote_steerable_changed={expected}.") - - -def _assert_not_unhandled(exc: Exception, method: str) -> None: - assert f"unhandled method {method}".lower() not in str(exc).lower() - - -class TestRpcRemote: - async def test_remote_off_is_noop_or_implemented_error(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - try: - result = await session.rpc.remote.enable( - RemoteEnableRequest(mode=RemoteSessionMode.OFF) - ) - except Exception as exc: - _assert_not_unhandled(exc, "session.remote.enable") - else: - assert result.remote_steerable is False - assert not result.url - finally: - await session.disconnect() - - async def test_remote_disable_is_noop_or_implemented_error(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - try: - await session.rpc.remote.disable() - except Exception as exc: - _assert_not_unhandled(exc, "session.remote.disable") - finally: - await session.disconnect() - - async def test_notify_steerable_changed_event_and_persist_flag(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - await session.rpc.remote.notify_steerable_changed( - RemoteNotifySteerableChangedRequest(remote_steerable=True) - ) - await _wait_for_remote_steerable_event(session, True) - - await session.rpc.remote.notify_steerable_changed( - RemoteNotifySteerableChangedRequest(remote_steerable=False) - ) - await _wait_for_remote_steerable_event(session, False) - finally: - await session.disconnect() diff --git a/python/e2e/test_rpc_schedule_e2e.py b/python/e2e/test_rpc_schedule_e2e.py deleted file mode 100644 index fdceac8dc4..0000000000 --- a/python/e2e/test_rpc_schedule_e2e.py +++ /dev/null @@ -1,37 +0,0 @@ -"""E2E coverage for session.schedule RPC methods.""" - -from __future__ import annotations - -import pytest - -from copilot.rpc import ScheduleStopRequest -from copilot.session import PermissionHandler - -from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -class TestRpcSchedule: - async def test_should_list_no_schedules_for_fresh_session(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - result = await session.rpc.schedule.list() - assert result.entries == [] - finally: - await session.disconnect() - - async def test_should_return_null_entry_when_stopping_unknown_schedule( - self, ctx: E2ETestContext - ): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - result = await session.rpc.schedule.stop(ScheduleStopRequest(id=2_147_483_647)) - assert result.entry is None - assert (await session.rpc.schedule.list()).entries == [] - finally: - await session.disconnect() diff --git a/python/e2e/test_rpc_server_e2e.py b/python/e2e/test_rpc_server_e2e.py deleted file mode 100644 index 044121e0cb..0000000000 --- a/python/e2e/test_rpc_server_e2e.py +++ /dev/null @@ -1,639 +0,0 @@ -""" -E2E coverage for top-level (server-scoped) RPC methods. - -Mirrors ``dotnet/test/RpcServerTests.cs`` (snapshot category ``rpc_server``). -""" - -from __future__ import annotations - -import os -import uuid -from datetime import UTC, datetime -from pathlib import Path - -import httpx -import pytest - -from copilot import CopilotClient, CopilotRequestContext, CopilotRequestHandler, RuntimeConnection -from copilot.rpc import ( - AccountGetQuotaRequest, - AgentsDiscoverRequest, - AgentsGetDiscoveryPathsRequest, - ConnectRemoteSessionParams, - InstructionsDiscoverRequest, - InstructionsGetDiscoveryPathsRequest, - LlmInferenceHTTPResponseChunkError, - LlmInferenceHTTPResponseChunkRequest, - LlmInferenceHTTPResponseStartRequest, - LocalSessionMetadataValue, - MCPDiscoverRequest, - ModelsListRequest, - PingRequest, - SecretsAddFilterValuesRequest, - SessionContext, - SessionFSSetProviderCapabilities, - SessionFSSetProviderConventions, - SessionFSSetProviderRequest, - SessionListFilter, - SessionsBulkDeleteRequest, - SessionsCheckInUseRequest, - SessionsCloseRequest, - SessionsEnrichMetadataRequest, - SessionsFindByPrefixRequest, - SessionsFindByTaskIDRequest, - SessionsGetLastForContextRequest, - SessionsListRequest, - SessionsLoadDeferredRepoHooksRequest, - SessionsPruneOldRequest, - SessionsReleaseLockRequest, - SessionsReloadPluginHooksRequest, - SessionsSaveRequest, - SessionsSetAdditionalPluginsRequest, - SkillsConfigSetDisabledSkillsRequest, - SkillsDiscoverRequest, - SkillsGetDiscoveryPathsRequest, - ToolsListRequest, -) -from copilot.session import PermissionHandler - -from ._copilot_request_helpers import ( - SYNTHETIC_TEXT, - assistant_text, - build_inference_response, - build_non_inference_response, - is_inference_url, -) -from .testharness import E2ETestContext, is_inprocess_transport - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -def _create_skill_directory(work_dir: str, skill_name: str, description: str) -> str: - skills_dir = Path(work_dir) / "server-rpc-skills" / uuid.uuid4().hex - skill_subdir = skills_dir / skill_name - skill_subdir.mkdir(parents=True, exist_ok=True) - skill_md = ( - f"---\n" - f"name: {skill_name}\n" - f"description: {description}\n" - f"---\n\n" - f"# {skill_name}\n\n" - f"This skill is used by RPC E2E tests.\n" - ) - (skill_subdir / "SKILL.md").write_text(skill_md, encoding="utf-8", newline="\n") - return str(skills_dir) - - -def _paths_equal(left: str, right: str | None) -> bool: - if right is None: - return False - return os.path.normcase(os.path.abspath(left)) == os.path.normcase(os.path.abspath(right)) - - -@pytest.fixture(scope="module") -async def authed_ctx(ctx: E2ETestContext): - """Configure proxy to redirect GitHub user lookups so per-token auth works.""" - ctx.add_runtime_env("COPILOT_DEBUG_GITHUB_API_URL", ctx.proxy_url) - return ctx - - -def _make_authed_client( - ctx: E2ETestContext, - token: str, - *, - request_handler: CopilotRequestHandler | None = None, -) -> CopilotClient: - env = ctx.get_env() - env["COPILOT_DEBUG_GITHUB_API_URL"] = ctx.proxy_url - return CopilotClient( - connection=RuntimeConnection.for_stdio(path=ctx.cli_path), - working_directory=ctx.work_dir, - env=env, - github_token=token, - request_handler=request_handler, - ) - - -class _PersistedSessionRequestHandler(CopilotRequestHandler): - """Complete the metadata fixture's real turn without a live inference request.""" - - async def send_request( - self, request: httpx.Request, ctx: CopilotRequestContext - ) -> httpx.Response: - if is_inference_url(str(request.url)): - return build_inference_response(request) - return build_non_inference_response(str(request.url), supported_endpoints=["/responses"]) - - -def _make_client_with_env(ctx: E2ETestContext, env_overrides: dict[str, str]) -> CopilotClient: - env = ctx.get_env() - env.update(env_overrides) - return CopilotClient( - connection=RuntimeConnection.for_stdio(path=ctx.cli_path), - working_directory=ctx.work_dir, - env=env, - github_token="fake-token-for-e2e-tests", - ) - - -async def _configure_user( - ctx: E2ETestContext, - token: str, - quota_snapshots: dict | None = None, -): - payload: dict = { - "login": "rpc-user", - "copilot_plan": "individual_pro", - "endpoints": { - "api": ctx.proxy_url, - "telemetry": "https://localhost:1/telemetry", - }, - "analytics_tracking_id": "rpc-user-tracking-id", - } - if quota_snapshots is not None: - payload["quota_snapshots"] = quota_snapshots - await ctx.set_copilot_user_by_token(token, payload) - - -class TestRpcServer: - async def test_should_call_rpc_ping_with_typed_params_and_result(self, ctx: E2ETestContext): - await ctx.client.start() - result = await ctx.client.rpc.ping(PingRequest(message="typed rpc test")) - assert result.message == "pong: typed rpc test" - assert result.timestamp is not None - - @pytest.mark.skipif( - is_inprocess_transport(), - reason="managedSettings.clearCache is unavailable in the in-process host", - ) - async def test_should_clear_the_managed_settings_cache(self, ctx: E2ETestContext): - await ctx.client.start() - assert await ctx.client.rpc.managed_settings.clear_cache() is None - - async def test_should_reject_llm_inference_response_frames_for_missing_request( - self, ctx: E2ETestContext - ): - await ctx.client.start() - - start = await ctx.client.rpc.llm_inference.http_response_start( - LlmInferenceHTTPResponseStartRequest( - request_id="missing-llm-inference-request", - status=200, - status_text="OK", - headers={"content-type": ["text/event-stream"]}, - ) - ) - assert start.accepted is False - - chunk = await ctx.client.rpc.llm_inference.http_response_chunk( - LlmInferenceHTTPResponseChunkRequest( - request_id="missing-llm-inference-request", - data="data: {}\n\n", - binary=False, - end=False, - ) - ) - assert chunk.accepted is False - - error = await ctx.client.rpc.llm_inference.http_response_chunk( - LlmInferenceHTTPResponseChunkRequest( - request_id="missing-llm-inference-request", - data="", - end=True, - error=LlmInferenceHTTPResponseChunkError( - message="No pending LLM inference request.", - code="missing_request", - ), - ) - ) - assert error.accepted is False - - async def test_should_call_rpc_models_list_with_typed_result(self, authed_ctx: E2ETestContext): - token = "rpc-models-token" - await _configure_user(authed_ctx, token) - client = _make_authed_client(authed_ctx, token) - try: - await client.start() - result = await client.rpc.models.list(ModelsListRequest()) - assert result.models is not None - assert any(model.id == "claude-sonnet-5" for model in result.models) - assert all((model.name or "").strip() for model in result.models) - finally: - try: - await client.stop() - except ExceptionGroup: - # Intentional: shutting down the per-test client can race the - # CLI's own teardown and surface as an aggregated cancellation - # error from anyio. We don't want it to fail the test. - pass - - async def test_should_call_rpc_account_get_quota_when_authenticated( - self, authed_ctx: E2ETestContext - ): - token = "rpc-quota-token" - await _configure_user( - authed_ctx, - token, - quota_snapshots={ - "chat": { - "entitlement": 100, - "overage_count": 2, - "overage_permitted": True, - "percent_remaining": 75, - "timestamp_utc": "2026-04-30T00:00:00Z", - } - }, - ) - client = _make_authed_client(authed_ctx, token) - try: - await client.start() - result = await client.rpc.account.get_quota(AccountGetQuotaRequest(git_hub_token=token)) - assert "chat" in result.quota_snapshots - chat_quota = result.quota_snapshots["chat"] - assert chat_quota.entitlement_requests == 100 - assert chat_quota.used_requests == 25 - assert chat_quota.remaining_percentage == 75 - assert chat_quota.overage == 2 - assert chat_quota.usage_allowed_with_exhausted_quota is True - assert chat_quota.overage_allowed_with_exhausted_quota is True - assert chat_quota.reset_date == "2026-04-30T00:00:00Z" - finally: - try: - await client.stop() - except ExceptionGroup: - # Intentional: shutting down the per-test client can race the - # CLI's own teardown and surface as an aggregated cancellation - # error from anyio. We don't want it to fail the test. - pass - - async def test_should_call_rpc_tools_list_with_typed_result(self, ctx: E2ETestContext): - await ctx.client.start() - result = await ctx.client.rpc.tools.list(ToolsListRequest()) - assert result.tools is not None - assert len(result.tools) > 0 - assert all((tool.name or "").strip() for tool in result.tools) - - async def test_should_call_rpc_session_fs_set_provider_with_typed_result( - self, ctx: E2ETestContext - ): - client = _make_client_with_env(ctx, {}) - try: - await client.start() - result = await client.rpc.session_fs.set_provider( - SessionFSSetProviderRequest( - initial_cwd="/", - session_state_path="/session-state", - conventions=SessionFSSetProviderConventions.POSIX, - capabilities=SessionFSSetProviderCapabilities(sqlite=True), - ) - ) - assert result.success is True - finally: - try: - await client.stop() - except ExceptionGroup: - # Intentional: shutting down the per-test client can race the - # CLI's own teardown and surface as an aggregated cancellation - # error from anyio. We don't want it to fail the test. - pass - - async def test_should_add_secret_filter_values(self, ctx: E2ETestContext): - client = _make_client_with_env(ctx, {"COPILOT_ENABLE_SECRET_FILTERING": "true"}) - try: - await client.start() - secret = f"rpc-secret-{uuid.uuid4().hex}" - result = await client.rpc.secrets.add_filter_values( - SecretsAddFilterValuesRequest(values=[secret]) - ) - assert result.ok is True - finally: - try: - await client.stop() - except ExceptionGroup: - # Intentional: shutting down the per-test client can race the - # CLI's own teardown and surface as an aggregated cancellation - # error from anyio. We don't want it to fail the test. - pass - - async def test_should_list_find_and_inspect_persisted_session_state( - self, authed_ctx: E2ETestContext - ): - token = os.environ.get("GITHUB_TOKEN", "fakevalue") - await _configure_user(authed_ctx, token) - client = _make_authed_client( - authed_ctx, token, request_handler=_PersistedSessionRequestHandler() - ) - - session_id = str(uuid.uuid4()) - working_directory = Path(authed_ctx.work_dir) / f"server-rpc-list-{uuid.uuid4().hex}" - working_directory.mkdir(parents=True, exist_ok=True) - missing_task_id = f"missing-task-{uuid.uuid4().hex}" - missing_session_id = str(uuid.uuid4()) - session = None - try: - await client.start() - session = await client.create_session( - session_id=session_id, - working_directory=str(working_directory), - on_permission_request=PermissionHandler.approve_all, - ) - - # A user turn makes sessions.list nonempty. Finish a synthetic turn - # before inspecting persistence or detaching; enqueue alone leaves - # unobserved inference racing cleanup. - message = await session.send_and_wait( - "Record a turn for sessions.list discriminator coverage", timeout=60.0 - ) - assert assistant_text(message) == SYNTHETIC_TEXT - save = await client.rpc.sessions.save(SessionsSaveRequest(session_id=session_id)) - assert save is not None - listed = await client.rpc.sessions.list( - SessionsListRequest( - filter=SessionListFilter(cwd=str(working_directory)), - metadata_limit=0, - ) - ) - - assert listed is not None - assert listed.sessions is not None - assert len(listed.sessions) >= 1 - matching = [item for item in listed.sessions if item.session_id == session_id] - assert len(matching) == 1 - assert isinstance(matching[0], LocalSessionMetadataValue) - assert matching[0].is_remote is False - assert all( - item.context is None - or os.path.normcase(os.path.abspath(item.context.cwd)) - == os.path.normcase(os.path.abspath(str(working_directory))) - for item in listed.sessions - ) - - by_prefix = await client.rpc.sessions.find_by_prefix( - SessionsFindByPrefixRequest(prefix=session_id[:8]) - ) - assert by_prefix.session_id in (None, session_id) - - by_task = await client.rpc.sessions.find_by_task_id( - SessionsFindByTaskIDRequest(task_id=missing_task_id) - ) - assert by_task.session_id is None - - last_for_context = await client.rpc.sessions.get_last_for_context( - SessionsGetLastForContextRequest(context=SessionContext(cwd=str(working_directory))) - ) - assert last_for_context.session_id in (None, session_id) - - sizes = await client.rpc.sessions.get_sizes() - assert sizes.sizes is not None - if session_id in sizes.sizes: - assert sizes.sizes[session_id] >= 0 - - in_use = await client.rpc.sessions.check_in_use( - SessionsCheckInUseRequest(session_ids=[session_id, missing_session_id]) - ) - assert missing_session_id not in in_use.in_use - finally: - try: - if session is not None: - await session.disconnect() - finally: - try: - await client.stop() - except ExceptionGroup: - # Intentional: shutting down the per-test client can race the - # CLI's own teardown and surface as an aggregated cancellation - # error from anyio. We don't want it to fail the test. - pass - - async def test_should_enrich_basic_session_metadata(self, ctx: E2ETestContext): - session_id = str(uuid.uuid4()) - working_directory = Path(ctx.work_dir) / f"server-rpc-enrich-{uuid.uuid4().hex}" - working_directory.mkdir(parents=True, exist_ok=True) - session = await ctx.client.create_session( - session_id=session_id, - working_directory=str(working_directory), - on_permission_request=PermissionHandler.approve_all, - ) - try: - await session.log("SERVER_RPC_ENRICH_READY") - await ctx.client.rpc.sessions.save(SessionsSaveRequest(session_id=session_id)) - - now = datetime.now(UTC).isoformat() - result = await ctx.client.rpc.sessions.enrich_metadata( - SessionsEnrichMetadataRequest( - sessions=[ - LocalSessionMetadataValue( - is_remote=False, - modified_time=now, - session_id=session_id, - start_time=now, - name="Basic metadata", - context=SessionContext(cwd=str(working_directory)), - ) - ] - ) - ) - - assert len(result.sessions) == 1 - enriched = result.sessions[0] - assert enriched.session_id == session_id - assert enriched.is_remote is False - assert enriched.context is not None - assert os.path.normcase(os.path.abspath(enriched.context.cwd)) == os.path.normcase( - os.path.abspath(str(working_directory)) - ) - finally: - await session.disconnect() - - async def test_should_close_release_prune_and_bulk_delete_persisted_session( - self, ctx: E2ETestContext - ): - session_id = str(uuid.uuid4()) - missing_session_id = str(uuid.uuid4()) - working_directory = Path(ctx.work_dir) / f"server-rpc-delete-{uuid.uuid4().hex}" - working_directory.mkdir(parents=True, exist_ok=True) - - session = await ctx.client.create_session( - session_id=session_id, - working_directory=str(working_directory), - on_permission_request=PermissionHandler.approve_all, - ) - await session.log("SERVER_RPC_DELETE_READY") - await ctx.client.rpc.sessions.save(SessionsSaveRequest(session_id=session_id)) - await ctx.client.rpc.sessions.close(SessionsCloseRequest(session_id=session_id)) - release = await ctx.client.rpc.sessions.release_lock( - SessionsReleaseLockRequest(session_id=session_id) - ) - assert release is not None - - prune = await ctx.client.rpc.sessions.prune_old( - SessionsPruneOldRequest( - older_than_days=0, - dry_run=True, - include_named=True, - exclude_session_ids=[], - ) - ) - assert prune.dry_run is True - assert missing_session_id not in prune.candidates - assert session_id not in prune.deleted - assert prune.freed_bytes >= 0 - - deleted = await ctx.client.rpc.sessions.bulk_delete( - SessionsBulkDeleteRequest(session_ids=[session_id, missing_session_id]) - ) - assert session_id in deleted.freed_bytes - assert deleted.freed_bytes[session_id] >= 0 - if missing_session_id in deleted.freed_bytes: - assert deleted.freed_bytes[missing_session_id] == 0 - - listed = await ctx.client.rpc.sessions.list(SessionsListRequest()) - assert all(item.session_id != session_id for item in listed.sessions) - - async def test_should_report_implemented_error_when_connecting_unknown_remote_session( - self, ctx: E2ETestContext - ): - await ctx.client.start() - remote_session_id = f"remote-{uuid.uuid4().hex}" - with pytest.raises(Exception) as excinfo: - await ctx.client.rpc.sessions.connect( - ConnectRemoteSessionParams(session_id=remote_session_id) - ) - text = str(excinfo.value).lower() - assert "unhandled method sessions.connect" not in text - assert remote_session_id.lower() in text or "session" in text - - async def test_should_set_additional_plugins_and_reload_deferred_hooks( - self, ctx: E2ETestContext - ): - await ctx.client.start() - cleared = await ctx.client.rpc.sessions.set_additional_plugins( - SessionsSetAdditionalPluginsRequest(plugins=[]) - ) - assert cleared is not None - - session_id = str(uuid.uuid4()) - working_directory = Path(ctx.work_dir) / f"server-rpc-hooks-{uuid.uuid4().hex}" - working_directory.mkdir(parents=True, exist_ok=True) - session = await ctx.client.create_session( - session_id=session_id, - working_directory=str(working_directory), - on_permission_request=PermissionHandler.approve_all, - enable_config_discovery=False, - ) - try: - reload_result = await ctx.client.rpc.sessions.reload_plugin_hooks( - SessionsReloadPluginHooksRequest(session_id=session_id, defer_repo_hooks=True) - ) - assert reload_result is not None - - loaded = await ctx.client.rpc.sessions.load_deferred_repo_hooks( - SessionsLoadDeferredRepoHooksRequest(session_id=session_id) - ) - assert loaded.hook_count == 0 - assert loaded.startup_prompts == [] - finally: - await ctx.client.rpc.sessions.set_additional_plugins( - SessionsSetAdditionalPluginsRequest(plugins=[]) - ) - await session.disconnect() - - async def test_should_discover_server_mcp_and_skills(self, ctx: E2ETestContext): - await ctx.client.start() - - skill_name = f"server-rpc-skill-{uuid.uuid4().hex}" - skill_directory = _create_skill_directory( - ctx.work_dir, - skill_name, - "Skill discovered by server-scoped RPC tests.", - ) - - mcp = await ctx.client.rpc.mcp.discover(MCPDiscoverRequest(working_directory=ctx.work_dir)) - assert mcp.servers is not None - - skills = await ctx.client.rpc.skills.discover( - SkillsDiscoverRequest(skill_directories=[skill_directory]) - ) - matching = [s for s in skills.skills if s.name == skill_name] - assert len(matching) == 1 - discovered = matching[0] - assert discovered.description == "Skill discovered by server-scoped RPC tests." - assert discovered.enabled is True - assert discovered.path.endswith(os.path.join(skill_name, "SKILL.md")) - - skill_paths = await ctx.client.rpc.skills.get_discovery_paths( - SkillsGetDiscoveryPathsRequest( - project_paths=[ctx.work_dir], - exclude_host_skills=True, - ) - ) - project_skill_path = next( - ( - path - for path in skill_paths.paths - if _paths_equal(ctx.work_dir, path.project_path) and path.preferred_for_creation - ), - None, - ) - assert project_skill_path is not None - assert project_skill_path.path.strip() - - agents = await ctx.client.rpc.agents.discover( - AgentsDiscoverRequest(project_paths=[ctx.work_dir], exclude_host_agents=True) - ) - assert all(agent.name.strip() for agent in agents.agents) - - agent_paths = await ctx.client.rpc.agents.get_discovery_paths( - AgentsGetDiscoveryPathsRequest( - project_paths=[ctx.work_dir], - exclude_host_agents=True, - ) - ) - project_agent_path = next( - ( - path - for path in agent_paths.paths - if _paths_equal(ctx.work_dir, path.project_path) and path.preferred_for_creation - ), - None, - ) - assert project_agent_path is not None - assert project_agent_path.path.strip() - - instructions = await ctx.client.rpc.instructions.discover( - InstructionsDiscoverRequest( - project_paths=[ctx.work_dir], - exclude_host_instructions=True, - ) - ) - assert all( - source.id.strip() and source.label.strip() and source.source_path.strip() - for source in instructions.sources - ) - - instruction_paths = await ctx.client.rpc.instructions.get_discovery_paths( - InstructionsGetDiscoveryPathsRequest( - project_paths=[ctx.work_dir], - exclude_host_instructions=True, - ) - ) - assert instruction_paths.paths - assert any( - _paths_equal(ctx.work_dir, path.project_path) for path in instruction_paths.paths - ) - assert all(path.path.strip() for path in instruction_paths.paths) - - try: - await ctx.client.rpc.skills.config.set_disabled_skills( - SkillsConfigSetDisabledSkillsRequest(disabled_skills=[skill_name]) - ) - disabled = await ctx.client.rpc.skills.discover( - SkillsDiscoverRequest(skill_directories=[skill_directory]) - ) - disabled_match = [s for s in disabled.skills if s.name == skill_name] - assert len(disabled_match) == 1 - assert disabled_match[0].enabled is False - finally: - await ctx.client.rpc.skills.config.set_disabled_skills( - SkillsConfigSetDisabledSkillsRequest(disabled_skills=[]) - ) diff --git a/python/e2e/test_rpc_server_misc_e2e.py b/python/e2e/test_rpc_server_misc_e2e.py deleted file mode 100644 index 2b6b7d9514..0000000000 --- a/python/e2e/test_rpc_server_misc_e2e.py +++ /dev/null @@ -1,243 +0,0 @@ -""" -E2E coverage for miscellaneous server-scoped RPC methods. - -Mirrors ``dotnet/test/E2E/RpcServerMiscE2ETests.cs`` (snapshot category -``rpc_server_misc``). -""" - -from __future__ import annotations - -import contextlib -import shutil -import uuid -from pathlib import Path - -import pytest - -from copilot import CopilotClient, RuntimeConnection -from copilot.rpc import ( - AccountLoginRequest, - AccountLogoutRequest, - AgentRegistrySpawnRequest, - SendAttachmentsToMessageParams, - SessionsOpenResumeLast, - SessionsOpenStatus, - UserSettingsSetRequest, -) -from copilot.session import PermissionHandler - -from .testharness import DEFAULT_GITHUB_TOKEN, E2ETestContext, wait_for_condition - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -def _create_dedicated_client(ctx: E2ETestContext) -> CopilotClient: - return CopilotClient( - connection=RuntimeConnection.for_stdio(path=ctx.cli_path), - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=DEFAULT_GITHUB_TOKEN, - ) - - -async def _create_isolated_client( - ctx: E2ETestContext, github_token: str | None = DEFAULT_GITHUB_TOKEN -) -> tuple[CopilotClient, Path]: - home = Path(ctx.work_dir) / f"copilot-e2e-misc-home-{uuid.uuid4().hex}" - home.mkdir(parents=True) - env = ctx.get_env() - for key in ("COPILOT_HOME", "GH_CONFIG_DIR", "XDG_CONFIG_HOME", "XDG_STATE_HOME"): - env[key] = str(home) - env["COPILOT_DEBUG_GITHUB_API_URL"] = ctx.proxy_url - if github_token is None: - env["GH_TOKEN"] = "" - env["GITHUB_TOKEN"] = "" - client = CopilotClient( - connection=RuntimeConnection.for_stdio(path=ctx.cli_path), - working_directory=ctx.work_dir, - env=env, - github_token=github_token, - use_logged_in_user=False if github_token is None else None, - ) - await client.start() - return client, home - - -async def _stop_client(client: CopilotClient) -> None: - with contextlib.suppress(ExceptionGroup, Exception): - await client.stop() - - -async def _dispose_isolated(client: CopilotClient, home: Path) -> None: - await _stop_client(client) - with contextlib.suppress(OSError): - shutil.rmtree(home, ignore_errors=True) - - -class TestRpcServerMisc: - async def test_should_reload_user_settings(self, ctx: E2ETestContext): - await ctx.client.start() - - await ctx.client.rpc.user.settings.reload() - - async def test_should_get_set_and_clear_user_settings(self, ctx: E2ETestContext): - client, home = await _create_isolated_client(ctx) - try: - before = await client.rpc.user.settings.get() - assert len(before.settings) > 0 - for key, setting in before.settings.items(): - assert key.strip() - assert isinstance(setting.is_default, bool) - - setting_key, setting = next( - (key, value) - for key, value in before.settings.items() - if isinstance(value.value, bool) - ) - toggled_value = setting.value is not True - - set_result = await client.rpc.user.settings.set( - UserSettingsSetRequest(settings={setting_key: toggled_value}) - ) - assert setting_key not in set_result.shadowed_keys - - await client.rpc.user.settings.reload() - after_set = await client.rpc.user.settings.get() - assert after_set.settings[setting_key].is_default is False - assert after_set.settings[setting_key].value is toggled_value - - await client.rpc.user.settings.set(UserSettingsSetRequest(settings={setting_key: None})) - await client.rpc.user.settings.reload() - after_clear = await client.rpc.user.settings.get() - assert after_clear.settings[setting_key].is_default is True - finally: - await _dispose_isolated(client, home) - - async def test_should_login_list_get_current_auth_and_logout_account(self, ctx: E2ETestContext): - login = f"rpc-account-{uuid.uuid4().hex}" - token = f"rpc-account-token-{uuid.uuid4().hex}" - await ctx.set_copilot_user_by_token( - token, - { - "login": login, - "copilot_plan": "individual_pro", - "endpoints": { - "api": ctx.proxy_url, - "telemetry": "https://localhost:1/telemetry", - }, - "analytics_tracking_id": "rpc-account-tracking-id", - }, - ) - - client, home = await _create_isolated_client(ctx, github_token=None) - try: - assert client._client is not None - initial_current = await client._client.request("account.getCurrentAuth", {}) - initial_auth_info = initial_current.get("authInfo") - assert initial_auth_info is not None - assert initial_auth_info.get("login") != login - - login_result = await client.rpc.account.login( - AccountLoginRequest(host="https://github.com", login=login, token=token) - ) - assert isinstance(login_result.stored_in_vault, bool) - - current = await client.rpc.account.get_current_auth() - assert current.auth_errors is None - assert current.auth_info is not None - assert current.auth_info.type == "user" - assert current.auth_info.host == "https://github.com" - assert current.auth_info.login == login - - users = await client.rpc.account.get_all_users() - assert isinstance(users, list) - account = next( - ( - user - for user in users - if user.get("authInfo", {}).get("type") == "user" - and user.get("authInfo", {}).get("login") == login - ), - None, - ) - if account is not None: - assert account.get("token") == token - - logout = await client.rpc.account.logout( - AccountLogoutRequest(auth_info=current.auth_info) - ) - assert logout.has_more_users is False - - users_after_logout = await client.rpc.account.get_all_users() - assert all( - user.get("authInfo", {}).get("login") != login for user in users_after_logout - ) - - current_after_logout = await client._client.request("account.getCurrentAuth", {}) - assert current_after_logout.get("authInfo") == initial_auth_info - finally: - await _dispose_isolated(client, home) - - async def test_should_report_agent_registry_spawn_gate_closed(self, ctx: E2ETestContext): - client, home = await _create_isolated_client(ctx) - try: - with pytest.raises(Exception) as excinfo: - await client.rpc.agent_registry.spawn(AgentRegistrySpawnRequest(cwd=ctx.work_dir)) - - message = str(excinfo.value) - assert "Unhandled method".lower() not in message.lower() - assert "agentRegistry.spawn".lower() in message.lower() - assert "not enabled" in message.lower() or "no delegate" in message.lower(), message - finally: - await _dispose_isolated(client, home) - - async def test_should_shut_down_owned_runtime(self, ctx: E2ETestContext): - client = _create_dedicated_client(ctx) - try: - await client.start() - await client.rpc.user.settings.reload() - - await client.rpc.runtime.shutdown() - - async def stopped_serving() -> bool: - try: - await client.rpc.user.settings.reload(timeout=1.0) - return False - except Exception: - return True - - await wait_for_condition( - stopped_serving, - timeout=15.0, - poll_interval=0.1, - timeout_message="Runtime kept serving RPCs after a graceful shutdown.", - ) - finally: - await _stop_client(client) - - async def test_should_report_not_found_when_opening_session_without_context( - self, ctx: E2ETestContext - ): - client, home = await _create_isolated_client(ctx) - try: - result = await client.rpc.sessions.open(SessionsOpenResumeLast()) - - assert result.status == SessionsOpenStatus.NOT_FOUND - assert result.session_id is None - finally: - await _dispose_isolated(client, home) - - async def test_should_reject_send_attachments_from_non_extension_connection( - self, ctx: E2ETestContext - ): - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) as session: - with pytest.raises(Exception) as excinfo: - await session.rpc.extensions.send_attachments_to_message( - SendAttachmentsToMessageParams(attachments=[]) - ) - - message = str(excinfo.value) - assert "Unhandled method".lower() not in message.lower() - assert "extension" in message.lower() diff --git a/python/e2e/test_rpc_server_plugins_e2e.py b/python/e2e/test_rpc_server_plugins_e2e.py deleted file mode 100644 index 538d1692fd..0000000000 --- a/python/e2e/test_rpc_server_plugins_e2e.py +++ /dev/null @@ -1,293 +0,0 @@ -""" -E2E coverage for server-scoped plugin and marketplace RPC methods. - -Mirrors ``dotnet/test/E2E/RpcServerPluginsE2ETests.cs`` (snapshot -category ``rpc_server_plugins``). -""" - -from __future__ import annotations - -import contextlib -import shutil -import uuid -from pathlib import Path - -import pytest - -from copilot import CopilotClient, RuntimeConnection -from copilot.rpc import ( - PluginsDisableRequest, - PluginsEnableRequest, - PluginsInstallRequest, - PluginsMarketplacesAddRequest, - PluginsMarketplacesBrowseRequest, - PluginsMarketplacesRefreshRequest, - PluginsMarketplacesRemoveRequest, - PluginsUninstallRequest, - PluginsUpdateRequest, -) - -from .testharness import DEFAULT_GITHUB_TOKEN, E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - -MARKETPLACE_NAME = "csharp-e2e-marketplace" -PLUGIN_NAME = "csharp-e2e-plugin" -DIRECT_PLUGIN_NAME = "csharp-e2e-direct" - - -def _write_skill_file(plugin_dir: Path) -> None: - skill = """--- -name: csharp-e2e-skill -description: A demo skill contributed by the E2E test plugin. ---- -# Demo Skill - -This skill exists so the plugin reports at least one installed skill. -""" - (plugin_dir / "SKILL.md").write_text(skill, encoding="utf-8", newline="\n") - - -def _create_local_marketplace_fixture(ctx: E2ETestContext) -> Path: - directory = Path(ctx.work_dir) / f"copilot-e2e-mp-{uuid.uuid4().hex}" - directory.mkdir(parents=True) - manifest = f"""{{ - "name": "{MARKETPLACE_NAME}", - "owner": {{ "name": "Copilot SDK E2E" }}, - "metadata": {{ "description": "Local marketplace fixture for SDK E2E tests." }}, - "plugins": [ - {{ - "name": "{PLUGIN_NAME}", - "source": "./{PLUGIN_NAME}", - "description": "E2E demo plugin advertised by the local marketplace.", - "version": "1.0.0" - }} - ] -}} -""" - (directory / "marketplace.json").write_text(manifest, encoding="utf-8", newline="\n") - plugin_dir = directory / PLUGIN_NAME - plugin_dir.mkdir() - _write_skill_file(plugin_dir) - return directory - - -def _create_direct_plugin_fixture(ctx: E2ETestContext) -> Path: - directory = Path(ctx.work_dir) / f"copilot-e2e-plugin-{uuid.uuid4().hex}" - directory.mkdir(parents=True) - manifest = f"""{{ - "name": "{DIRECT_PLUGIN_NAME}", - "description": "E2E demo plugin installed directly from a local path.", - "version": "1.0.0" -}} -""" - (directory / "plugin.json").write_text(manifest, encoding="utf-8", newline="\n") - _write_skill_file(directory) - return directory - - -async def _create_isolated_client(ctx: E2ETestContext) -> tuple[CopilotClient, Path]: - home = Path(ctx.work_dir) / f"copilot-e2e-home-{uuid.uuid4().hex}" - home.mkdir(parents=True) - env = ctx.get_env() - for key in ("COPILOT_HOME", "GH_CONFIG_DIR", "XDG_CONFIG_HOME", "XDG_STATE_HOME"): - env[key] = str(home) - client = CopilotClient( - connection=RuntimeConnection.for_stdio(path=ctx.cli_path), - working_directory=ctx.work_dir, - env=env, - github_token=DEFAULT_GITHUB_TOKEN, - ) - await client.start() - return client, home - - -async def _dispose_isolated(client: CopilotClient, home: Path, fixture_dir: Path | None) -> None: - with contextlib.suppress(ExceptionGroup): - await client.stop() - with contextlib.suppress(OSError): - shutil.rmtree(home, ignore_errors=True) - if fixture_dir is not None: - with contextlib.suppress(OSError): - shutil.rmtree(fixture_dir, ignore_errors=True) - - -class TestRpcServerPlugins: - async def test_should_install_and_list_plugin_from_local_marketplace(self, ctx: E2ETestContext): - marketplace_dir = _create_local_marketplace_fixture(ctx) - client, home = await _create_isolated_client(ctx) - try: - await client.rpc.plugins.marketplaces.add( - PluginsMarketplacesAddRequest(source=str(marketplace_dir)) - ) - - spec = f"{PLUGIN_NAME}@{MARKETPLACE_NAME}" - install = await client.rpc.plugins.install(PluginsInstallRequest(source=spec)) - - assert install.plugin.name == PLUGIN_NAME - assert install.plugin.marketplace == MARKETPLACE_NAME - assert install.plugin.enabled is True - assert install.skills_installed >= 1 - assert install.deprecation_warning is None - - after_install = await client.rpc.plugins.list() - listed = [ - p - for p in after_install.plugins - if p.name == PLUGIN_NAME and p.marketplace == MARKETPLACE_NAME - ] - assert len(listed) == 1 - assert listed[0].enabled is True - - finally: - await _dispose_isolated(client, home, marketplace_dir) - - async def test_should_enable_and_disable_marketplace_plugin(self, ctx: E2ETestContext): - marketplace_dir = _create_local_marketplace_fixture(ctx) - client, home = await _create_isolated_client(ctx) - try: - spec = f"{PLUGIN_NAME}@{MARKETPLACE_NAME}" - await client.rpc.plugins.marketplaces.add( - PluginsMarketplacesAddRequest(source=str(marketplace_dir)) - ) - await client.rpc.plugins.install(PluginsInstallRequest(source=spec)) - - await client.rpc.plugins.disable(PluginsDisableRequest(names=[spec])) - assert _single_marketplace_plugin(await client.rpc.plugins.list()).enabled is False - - await client.rpc.plugins.enable(PluginsEnableRequest(names=[spec])) - assert _single_marketplace_plugin(await client.rpc.plugins.list()).enabled is True - finally: - await _dispose_isolated(client, home, marketplace_dir) - - async def test_should_update_single_marketplace_plugin(self, ctx: E2ETestContext): - marketplace_dir = _create_local_marketplace_fixture(ctx) - client, home = await _create_isolated_client(ctx) - try: - spec = f"{PLUGIN_NAME}@{MARKETPLACE_NAME}" - await client.rpc.plugins.marketplaces.add( - PluginsMarketplacesAddRequest(source=str(marketplace_dir)) - ) - await client.rpc.plugins.install(PluginsInstallRequest(source=spec)) - - update = await client.rpc.plugins.update(PluginsUpdateRequest(name=spec)) - - assert update.skills_installed >= 1 - assert update.previous_version == "1.0.0" - assert update.new_version == "1.0.0" - finally: - await _dispose_isolated(client, home, marketplace_dir) - - async def test_should_update_all_installed_plugins(self, ctx: E2ETestContext): - marketplace_dir = _create_local_marketplace_fixture(ctx) - client, home = await _create_isolated_client(ctx) - try: - spec = f"{PLUGIN_NAME}@{MARKETPLACE_NAME}" - await client.rpc.plugins.marketplaces.add( - PluginsMarketplacesAddRequest(source=str(marketplace_dir)) - ) - await client.rpc.plugins.install(PluginsInstallRequest(source=spec)) - - result = await client.rpc.plugins.update_all() - - entries = [ - r - for r in result.results - if r.name == PLUGIN_NAME and r.marketplace == MARKETPLACE_NAME - ] - assert len(entries) == 1 - entry = entries[0] - assert entry.success is True, entry.error - assert entry.skills_installed is not None and entry.skills_installed >= 1 - finally: - await _dispose_isolated(client, home, marketplace_dir) - - async def test_should_install_direct_local_plugin_with_deprecation_warning( - self, ctx: E2ETestContext - ): - plugin_dir = _create_direct_plugin_fixture(ctx) - client, home = await _create_isolated_client(ctx) - try: - install = await client.rpc.plugins.install( - PluginsInstallRequest(source=str(plugin_dir)) - ) - - assert install.plugin.name == DIRECT_PLUGIN_NAME - assert install.plugin.marketplace == "" - assert install.deprecation_warning is not None - assert "deprecated" in install.deprecation_warning.lower() - assert install.skills_installed >= 1 - - after_install = await client.rpc.plugins.list() - assert len([p for p in after_install.plugins if p.name == DIRECT_PLUGIN_NAME]) == 1 - assert install.plugin.direct_source_id - - await client.rpc.plugins.uninstall( - PluginsUninstallRequest( - name=DIRECT_PLUGIN_NAME, - direct_source_id=install.plugin.direct_source_id, - ) - ) - - after_uninstall = await client.rpc.plugins.list() - assert not any(p.name == DIRECT_PLUGIN_NAME for p in after_uninstall.plugins) - finally: - await _dispose_isolated(client, home, plugin_dir) - - async def test_should_list_browse_refresh_and_remove_local_marketplace( - self, ctx: E2ETestContext - ): - marketplace_dir = _create_local_marketplace_fixture(ctx) - client, home = await _create_isolated_client(ctx) - try: - add = await client.rpc.plugins.marketplaces.add( - PluginsMarketplacesAddRequest(source=str(marketplace_dir)) - ) - assert add.name == MARKETPLACE_NAME - - marketplaces = await client.rpc.plugins.marketplaces.list() - mine = [m for m in marketplaces.marketplaces if m.name == MARKETPLACE_NAME] - assert len(mine) == 1 - assert mine[0].is_default is not True - assert any(m.is_default is True for m in marketplaces.marketplaces) - - browse = await client.rpc.plugins.marketplaces.browse( - PluginsMarketplacesBrowseRequest(name=MARKETPLACE_NAME) - ) - advertised = [p for p in browse.plugins if p.name == PLUGIN_NAME] - assert len(advertised) == 1 - assert (advertised[0].description or "").strip() - - refresh = await client.rpc.plugins.marketplaces.refresh( - PluginsMarketplacesRefreshRequest(name=MARKETPLACE_NAME) - ) - refreshed = [r for r in refresh.results if r.name == MARKETPLACE_NAME] - assert len(refreshed) == 1 - assert refreshed[0].success is True, refreshed[0].error - - remove = await client.rpc.plugins.marketplaces.remove( - PluginsMarketplacesRemoveRequest(name=MARKETPLACE_NAME) - ) - assert remove.removed is True - - after_remove = await client.rpc.plugins.marketplaces.list() - assert not any(m.name == MARKETPLACE_NAME for m in after_remove.marketplaces) - finally: - await _dispose_isolated(client, home, marketplace_dir) - - async def test_should_reload_mcp_config_cache(self, ctx: E2ETestContext): - client, home = await _create_isolated_client(ctx) - try: - await client.rpc.mcp.config.reload() - finally: - await _dispose_isolated(client, home, None) - - -def _single_marketplace_plugin(plugin_list): - plugins = [ - p - for p in plugin_list.plugins - if p.name == PLUGIN_NAME and p.marketplace == MARKETPLACE_NAME - ] - assert len(plugins) == 1 - return plugins[0] diff --git a/python/e2e/test_rpc_server_remote_control_e2e.py b/python/e2e/test_rpc_server_remote_control_e2e.py deleted file mode 100644 index 0fe2cc1b37..0000000000 --- a/python/e2e/test_rpc_server_remote_control_e2e.py +++ /dev/null @@ -1,130 +0,0 @@ -""" -E2E coverage for server-scoped remote-control RPC methods. - -Mirrors ``dotnet/test/E2E/RpcServerRemoteControlE2ETests.cs`` (snapshot -category ``rpc_server_remote_control``). -""" - -from __future__ import annotations - -import contextlib -import uuid - -import pytest - -from copilot import CopilotClient, RuntimeConnection -from copilot.rpc import ( - RemoteControlConfig, - RemoteControlStatusOff, - SessionsSetRemoteControlSteeringRequest, - SessionsStartRemoteControlRequest, - SessionsStopRemoteControlRequest, - SessionsTransferRemoteControlRequest, -) - -from .testharness import DEFAULT_GITHUB_TOKEN, E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -def _create_dedicated_client(ctx: E2ETestContext) -> CopilotClient: - return CopilotClient( - connection=RuntimeConnection.for_stdio(path=ctx.cli_path), - working_directory=ctx.work_dir, - env=ctx.get_env(), - github_token=DEFAULT_GITHUB_TOKEN, - ) - - -async def _stop_client(client: CopilotClient) -> None: - with contextlib.suppress(ExceptionGroup): - await client.stop() - - -class TestRpcServerRemoteControl: - async def test_should_report_remote_control_status_as_off(self, ctx: E2ETestContext): - client = _create_dedicated_client(ctx) - try: - await client.start() - - result = await client.rpc.sessions.get_remote_control_status() - - assert isinstance(result.status, RemoteControlStatusOff) - assert result.status.state == "off" - finally: - await _stop_client(client) - - async def test_should_treat_set_steering_as_no_op_when_off(self, ctx: E2ETestContext): - client = _create_dedicated_client(ctx) - try: - await client.start() - - result = await client.rpc.sessions.set_remote_control_steering( - SessionsSetRemoteControlSteeringRequest(enabled=False) - ) - - assert isinstance(result.status, RemoteControlStatusOff) - finally: - await _stop_client(client) - - async def test_should_report_not_stopped_when_remote_control_is_off(self, ctx: E2ETestContext): - client = _create_dedicated_client(ctx) - try: - await client.start() - - result = await client.rpc.sessions.stop_remote_control( - SessionsStopRemoteControlRequest() - ) - - assert result.stopped is False - assert isinstance(result.status, RemoteControlStatusOff) - finally: - await _stop_client(client) - - async def test_should_reject_transfer_when_off_with_compare_and_swap(self, ctx: E2ETestContext): - client = _create_dedicated_client(ctx) - try: - await client.start() - - result = await client.rpc.sessions.transfer_remote_control( - SessionsTransferRemoteControlRequest( - to_session_id=f"rc-to-{uuid.uuid4().hex}", - expected_from_session_id=f"rc-from-{uuid.uuid4().hex}", - ) - ) - - assert result.transferred is False - assert isinstance(result.status, RemoteControlStatusOff) - finally: - await _stop_client(client) - - async def test_should_reach_runtime_when_starting_remote_control_for_unknown_session( - self, ctx: E2ETestContext - ): - client = _create_dedicated_client(ctx) - try: - await client.start() - - try: - with pytest.raises(Exception) as excinfo: - await client.rpc.sessions.start_remote_control( - SessionsStartRemoteControlRequest( - session_id=f"missing-session-{uuid.uuid4().hex}", - config=RemoteControlConfig( - explicit=False, - remote=False, - silent=True, - steerable=False, - ), - ) - ) - message = str(excinfo.value) - assert "Unhandled method".lower() not in message.lower() - assert "session" in message.lower() or "remote" in message.lower(), message - finally: - with contextlib.suppress(Exception): - await client.rpc.sessions.stop_remote_control( - SessionsStopRemoteControlRequest(force=True) - ) - finally: - await _stop_client(client) diff --git a/python/e2e/test_rpc_session_state_e2e.py b/python/e2e/test_rpc_session_state_e2e.py deleted file mode 100644 index 622192cfe9..0000000000 --- a/python/e2e/test_rpc_session_state_e2e.py +++ /dev/null @@ -1,930 +0,0 @@ -""" -E2E coverage for session-scoped state RPCs. - -Mirrors ``dotnet/test/RpcSessionStateTests.cs`` (snapshot category -``rpc_session_state``). -""" - -from __future__ import annotations - -import asyncio -import contextlib -import os -import time -import uuid -from pathlib import Path - -import pytest - -from copilot.rpc import ( - AuthInfoType, - CopilotUserResponse, - CopilotUserResponseEndpoints, - HistoryTruncateRequest, - HostType, - LspInitializeRequest, - MCPOauthLoginRequest, - MetadataContextInfoRequest, - MetadataRecomputeContextTokensRequest, - MetadataRecordContextChangeRequest, - MetadataSetWorkingDirectoryRequest, - ModelSetReasoningEffortRequest, - ModelSwitchToRequest, - ModeSetRequest, - NameSetAutoRequest, - NameSetRequest, - PermissionsResetSessionApprovalsRequest, - PermissionsSetApproveAllRequest, - PlanUpdateRequest, - SessionSetCredentialsParams, - SessionsForkRequest, - SessionUpdateOptionsParams, - SessionWorkingDirectoryContext, - ShutdownRequest, - TelemetrySetFeatureOverridesRequest, - UserAuthInfo, - WorkspacesCreateFileRequest, - WorkspacesReadFileRequest, -) -from copilot.session import PermissionHandler -from copilot.session_events import ( - AssistantMessageData, - SessionContextChangedData, - SessionMode, - SessionShutdownData, - SessionTitleChangedData, - ShutdownType, - UserMessageData, -) - -from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -def _conversation_messages(events) -> list[tuple[str, str]]: - out: list[tuple[str, str]] = [] - for evt in events: - match evt.data: - case UserMessageData() as data: - out.append(("user", data.content or "")) - case AssistantMessageData() as data: - out.append(("assistant", data.content or "")) - return out - - -def _path_equals(expected: str, actual: str | None) -> bool: - if actual is None: - return False - return os.path.normcase(os.path.abspath(expected)) == os.path.normcase(os.path.abspath(actual)) - - -def _create_unique_directory(ctx: E2ETestContext, prefix: str) -> str: - path = Path(ctx.work_dir) / f"{prefix}-{uuid.uuid4().hex}" - path.mkdir(parents=True, exist_ok=True) - return str(path) - - -async def _wait_for(condition, *, timeout: float = 15.0, message: str): - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - if await condition(): - return - await asyncio.sleep(0.2) - pytest.fail(message) - - -async def _assert_implemented_failure(awaitable, method: str) -> None: - with pytest.raises(Exception) as excinfo: - _ = await awaitable - assert f"Unhandled method {method}".lower() not in str(excinfo.value).lower() - - -class TestRpcSessionState: - async def test_should_call_session_rpc_model_get_current(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-5", - ) - try: - result = await session.rpc.model.get_current() - assert result.model_id - finally: - await session.disconnect() - - async def test_should_call_session_rpc_model_switchto(self, ctx: E2ETestContext): - # The runtime caches /models per (auth, base_url) for 30 minutes (see - # capi_client.rs LIST_MODELS_CACHE). Tests in this class share one CLI - # subprocess and proxy URL via the module-scoped `ctx` fixture, so the - # first snapshot's models list is reused by every later test. switch_to - # needs gpt-5.4 in the cache; rather than poisoning every other snapshot - # we spin up an isolated context with its own subprocess and proxy → its - # own (auth, base_url) cache key. - isolated_ctx = E2ETestContext() - await isolated_ctx.setup() - try: - await isolated_ctx.configure_for_test( - "rpc_session_state", "should_call_session_rpc_model_switchto" - ) - session = await isolated_ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-5", - ) - try: - before = await session.rpc.model.get_current() - assert before.model_id - - result = await session.rpc.model.switch_to( - ModelSwitchToRequest(model_id="gpt-5.4", reasoning_effort="high") - ) - assert result.model_id == "gpt-5.4" - - after = await session.rpc.model.get_current() - assert after.model_id == "gpt-5.4" - finally: - await session.disconnect() - finally: - await isolated_ctx.teardown() - - async def test_should_get_and_set_session_mode(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - initial = await session.rpc.mode.get() - assert initial == SessionMode.INTERACTIVE - - await session.rpc.mode.set(ModeSetRequest(mode=SessionMode.PLAN)) - assert await session.rpc.mode.get() == SessionMode.PLAN - - await session.rpc.mode.set(ModeSetRequest(mode=SessionMode.INTERACTIVE)) - assert await session.rpc.mode.get() == SessionMode.INTERACTIVE - finally: - await session.disconnect() - - async def test_should_shutdown_session_with_routine_type(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - shutdown_future: asyncio.Future = asyncio.get_event_loop().create_future() - - def on_event(event): - if ( - isinstance(event.data, SessionShutdownData) - and event.data.shutdown_type == ShutdownType.ROUTINE - and not shutdown_future.done() - ): - shutdown_future.set_result(event) - - unsubscribe = session.on(on_event) - try: - await session.rpc.shutdown( - ShutdownRequest( - type=ShutdownType.ROUTINE, - reason="SDK E2E shutdown coverage", - ) - ) - shutdown = await asyncio.wait_for(shutdown_future, timeout=15.0) - assert shutdown.data.shutdown_type == ShutdownType.ROUTINE - finally: - unsubscribe() - with contextlib.suppress(Exception): - await session.disconnect() - - async def test_should_read_update_and_delete_plan(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - initial = await session.rpc.plan.read() - assert initial.exists is False - assert initial.content is None - - plan_content = "# Test Plan\n\n- Step 1\n- Step 2" - await session.rpc.plan.update(PlanUpdateRequest(content=plan_content)) - - after_update = await session.rpc.plan.read() - assert after_update.exists is True - assert after_update.content == plan_content - - await session.rpc.plan.delete() - - after_delete = await session.rpc.plan.read() - assert after_delete.exists is False - assert after_delete.content is None - finally: - await session.disconnect() - - async def test_should_call_workspace_file_rpc_methods(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - initial = await session.rpc.workspaces.list_files() - assert initial.files is not None - - await session.rpc.workspaces.create_file( - WorkspacesCreateFileRequest(path="test.txt", content="Hello, workspace!") - ) - - after_create = await session.rpc.workspaces.list_files() - assert "test.txt" in after_create.files - - file = await session.rpc.workspaces.read_file( - WorkspacesReadFileRequest(path="test.txt") - ) - assert file.content == "Hello, workspace!" - - workspace = await session.rpc.workspaces.get_workspace() - assert workspace.workspace is not None - assert workspace.workspace.id is not None - finally: - await session.disconnect() - - async def test_should_get_and_set_session_metadata(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - await session.rpc.name.set(NameSetRequest(name="SDK test session")) - name = await session.rpc.name.get() - assert name.name == "SDK test session" - - sources = await session.rpc.instructions.get_sources() - assert sources.sources is not None - finally: - await session.disconnect() - - async def test_should_call_metadata_snapshot_set_working_directory_and_record_context_change( - self, ctx: E2ETestContext - ): - first_dir = _create_unique_directory(ctx, "metadata-first") - second_dir = _create_unique_directory(ctx, "metadata-second") - branch = f"rpc-context-{uuid.uuid4().hex}" - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-5", - working_directory=first_dir, - ) - try: - snapshot = await session.rpc.metadata.snapshot() - assert snapshot.session_id == session.session_id - assert snapshot.selected_model == "claude-sonnet-5" - assert snapshot.is_remote is False - assert snapshot.already_in_use is False - assert _path_equals(first_dir, snapshot.working_directory) - assert snapshot.workspace is not None - assert snapshot.workspace.id == session.session_id - assert snapshot.workspace_path - - set_result = await session.rpc.metadata.set_working_directory( - MetadataSetWorkingDirectoryRequest(working_directory=second_dir) - ) - assert _path_equals(second_dir, set_result.working_directory) - - async def snapshot_updated() -> bool: - current = await session.rpc.metadata.snapshot() - return _path_equals(second_dir, current.working_directory) - - await _wait_for( - snapshot_updated, - message="Timed out waiting for metadata snapshot cwd update.", - ) - - context_future: asyncio.Future = asyncio.get_event_loop().create_future() - - def on_event(event): - if ( - isinstance(event.data, SessionContextChangedData) - and event.data.branch == branch - and not context_future.done() - ): - context_future.set_result(event) - - unsubscribe = session.on(on_event) - try: - # For local sessions the CLI treats the session cwd as authoritative, so a - # record_context_change that reports a divergent cwd is ignored and emits - # no event. Report the current working directory (second_dir) to observe it. - result = await session.rpc.metadata.record_context_change( - MetadataRecordContextChangeRequest( - context=SessionWorkingDirectoryContext( - cwd=second_dir, - git_root=first_dir, - branch=branch, - repository="github/copilot-sdk-e2e", - repository_host="github.com", - host_type=HostType.GITHUB, - base_commit="0" * 40, - head_commit="1" * 40, - ) - ) - ) - assert result is not None - - event = await asyncio.wait_for(context_future, timeout=15.0) - assert _path_equals(second_dir, event.data.cwd) - assert _path_equals(first_dir, event.data.git_root) - assert event.data.branch == branch - assert event.data.repository == "github/copilot-sdk-e2e" - assert event.data.repository_host == "github.com" - assert event.data.host_type.value == "github" - assert event.data.base_commit == "0" * 40 - assert event.data.head_commit == "1" * 40 - finally: - unsubscribe() - finally: - await session.disconnect() - - async def test_should_update_options_initialize_services_and_set_feature_overrides( - self, ctx: E2ETestContext - ): - initial_dir = _create_unique_directory(ctx, "options-initial") - options_dir = _create_unique_directory(ctx, "options-updated") - feature_name = f"rpc-session-state-{uuid.uuid4().hex}" - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - working_directory=initial_dir, - ) - try: - update = await session.rpc.options.update( - SessionUpdateOptionsParams( - client_name="python-sdk-rpc-session-state-e2e", - lsp_client_name="python-sdk-rpc-session-state-lsp", - integration_id=f"python-sdk-{uuid.uuid4().hex}", - feature_flags={feature_name: True}, - working_directory=options_dir, - coauthor_enabled=False, - enable_streaming=False, - ask_user_disabled=True, - ) - ) - assert update.success is True - - async def snapshot_updated() -> bool: - snapshot = await session.rpc.metadata.snapshot() - return _path_equals(options_dir, snapshot.working_directory) - - await _wait_for( - snapshot_updated, - message="Timed out waiting for options.update cwd to reach metadata snapshot.", - ) - - await session.rpc.lsp.initialize( - LspInitializeRequest( - working_directory=options_dir, - git_root=initial_dir, - force=True, - ) - ) - await session.rpc.telemetry.set_feature_overrides( - TelemetrySetFeatureOverridesRequest( - features={ - "rpc_session_state_feature": feature_name, - "rpc_session_state_value": "enabled", - } - ) - ) - tools = await session.rpc.tools.initialize_and_validate() - assert tools is not None - finally: - await session.disconnect() - - async def test_should_set_reasoning_effort_and_auto_name(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-5", - ) - try: - reasoning = await session.rpc.model.set_reasoning_effort( - ModelSetReasoningEffortRequest(reasoning_effort="high") - ) - assert reasoning.reasoning_effort == "high" - current = await session.rpc.model.get_current() - assert current.model_id == "claude-sonnet-5" - assert current.reasoning_effort == "high" - - auto_name = f"Auto Session {uuid.uuid4().hex}" - title_future: asyncio.Future = asyncio.get_event_loop().create_future() - - def on_event(event): - if ( - isinstance(event.data, SessionTitleChangedData) - and event.data.title == auto_name - and not title_future.done() - ): - title_future.set_result(event) - - unsubscribe = session.on(on_event) - try: - auto = await session.rpc.name.set_auto( - NameSetAutoRequest(summary=f" {auto_name} ") - ) - assert auto.applied is True - await asyncio.wait_for(title_future, timeout=15.0) - finally: - unsubscribe() - - assert (await session.rpc.name.get()).name == auto_name - - explicit_name = f"Explicit Session {uuid.uuid4().hex}" - await session.rpc.name.set(NameSetRequest(name=explicit_name)) - ignored = await session.rpc.name.set_auto( - NameSetAutoRequest(summary=f"Ignored {uuid.uuid4().hex}") - ) - assert ignored.applied is False - assert (await session.rpc.name.get()).name == explicit_name - finally: - await session.disconnect() - - async def test_should_set_auth_credentials(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - login = f"sdk-rpc-{uuid.uuid4().hex}" - result = await session.rpc.git_hub_auth.set_credentials( - SessionSetCredentialsParams( - credentials=UserAuthInfo( - host="https://github.com", - login=login, - copilot_user=CopilotUserResponse( - analytics_tracking_id="rpc-session-state-tracking-id", - chat_enabled=True, - copilot_plan="individual_pro", - endpoints=CopilotUserResponseEndpoints( - api=ctx.proxy_url, - telemetry="https://localhost:1/telemetry", - ), - login=login, - ), - ) - ) - ) - assert result.success is True - - status = await session.rpc.git_hub_auth.get_status() - assert status.is_authenticated is True - assert status.auth_type == AuthInfoType.USER - assert status.host == "https://github.com" - assert status.login == login - finally: - await session.disconnect() - - async def test_should_fork_session_with_persisted_messages(self, ctx: E2ETestContext): - source_prompt = "Say FORK_SOURCE_ALPHA exactly." - fork_prompt = "Now say FORK_CHILD_BETA exactly." - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - initial_answer = await session.send_and_wait(source_prompt, timeout=60.0) - assert initial_answer is not None - assert "FORK_SOURCE_ALPHA" in (initial_answer.data.content or "") - - source_messages = await session.get_events() - source_conversation = _conversation_messages(source_messages) - assert any( - role == "user" and content == source_prompt for role, content in source_conversation - ) - assert any( - role == "assistant" and "FORK_SOURCE_ALPHA" in content - for role, content in source_conversation - ) - - fork = await ctx.client.rpc.sessions.fork( - SessionsForkRequest(session_id=session.session_id) - ) - assert (fork.session_id or "").strip() - assert fork.session_id != session.session_id - - forked_session = await ctx.client.resume_session( - fork.session_id, - on_permission_request=PermissionHandler.approve_all, - ) - try: - forked_messages = await forked_session.get_events() - forked_conversation = _conversation_messages(forked_messages) - assert forked_conversation[: len(source_conversation)] == source_conversation - - fork_answer = await forked_session.send_and_wait(fork_prompt, timeout=60.0) - assert fork_answer is not None - assert "FORK_CHILD_BETA" in (fork_answer.data.content or "") - - source_after_fork = _conversation_messages(await session.get_events()) - assert all(content != fork_prompt for _, content in source_after_fork) - - fork_after_prompt = _conversation_messages(await forked_session.get_events()) - assert any( - role == "user" and content == fork_prompt for role, content in fork_after_prompt - ) - assert any( - role == "assistant" and "FORK_CHILD_BETA" in content - for role, content in fork_after_prompt - ) - finally: - await forked_session.disconnect() - finally: - await session.disconnect() - - async def test_should_handle_forking_session_without_persisted_events( - self, ctx: E2ETestContext - ): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - try: - fork = await ctx.client.rpc.sessions.fork( - SessionsForkRequest(session_id=session.session_id) - ) - except Exception as exc: - text = str(exc).lower() - assert "not found or has no persisted events" in text - assert "unhandled method sessions.fork" not in text - return - - assert fork.session_id.strip() - assert fork.session_id != session.session_id - - forked_session = await ctx.client.resume_session( - fork.session_id, - on_permission_request=PermissionHandler.approve_all, - ) - try: - assert _conversation_messages(await forked_session.get_events()) == [] - finally: - await forked_session.disconnect() - finally: - await session.disconnect() - - async def test_should_call_session_usage_and_permission_rpcs(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - metrics = await session.rpc.usage.get_metrics() - assert metrics.session_start_time is not None - if metrics.total_nano_aiu is not None: - assert metrics.total_nano_aiu >= 0 - if metrics.token_details is not None: - for detail in metrics.token_details.values(): - assert detail.token_count >= 0 - for model_metric in metrics.model_metrics.values(): - if model_metric.total_nano_aiu is not None: - assert model_metric.total_nano_aiu >= 0 - if model_metric.token_details is not None: - for detail in model_metric.token_details.values(): - assert detail.token_count >= 0 - - handoff = await session.rpc.history.summarize_for_handoff() - assert isinstance(handoff.summary, str) - - cancel_background = await session.rpc.history.cancel_background_compaction() - assert cancel_background.cancelled is False - - abort_manual = await session.rpc.history.abort_manual_compaction() - assert abort_manual.aborted is False - - try: - approve_all = await session.rpc.permissions.set_approve_all( - PermissionsSetApproveAllRequest(enabled=True) - ) - assert approve_all.success - - reset = await session.rpc.permissions.reset_session_approvals( - PermissionsResetSessionApprovalsRequest() - ) - assert reset.success - finally: - await session.rpc.permissions.set_approve_all( - PermissionsSetApproveAllRequest(enabled=False) - ) - finally: - await session.disconnect() - - async def test_should_report_implemented_errors_for_unsupported_session_rpc_paths( - self, ctx: E2ETestContext - ): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - await _assert_implemented_failure( - session.rpc.history.truncate(HistoryTruncateRequest(event_id="missing-event")), - "session.history.truncate", - ) - await _assert_implemented_failure( - session.rpc.mcp.oauth.login(MCPOauthLoginRequest(server_name="missing-server")), - "session.mcp.oauth.login", - ) - finally: - await session.disconnect() - - async def test_should_compact_session_history_after_messages(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - assert (await session.rpc.metadata.is_processing()).processing is False - await session.send_and_wait("What is 2+2?", timeout=60.0) - assert (await session.rpc.metadata.is_processing()).processing is False - - context_info = await session.rpc.metadata.context_info( - MetadataContextInfoRequest( - prompt_token_limit=128_000, - output_token_limit=4_096, - selected_model="claude-sonnet-5", - ) - ) - if context_info.context_info is not None: - context = context_info.context_info - assert context.model_name == "claude-sonnet-5" - assert context.prompt_token_limit == 128_000 - assert context.limit >= context.prompt_token_limit - assert context.total_tokens > 0 - assert context.system_tokens > 0 - assert context.conversation_tokens > 0 - assert context.tool_definitions_tokens >= 0 - assert ( - context.system_tokens - + context.conversation_tokens - + context.tool_definitions_tokens - == context.total_tokens - ) - - recomputed = await session.rpc.metadata.recompute_context_tokens( - MetadataRecomputeContextTokensRequest(model_id="claude-sonnet-5") - ) - assert recomputed.system_token_count > 0 - assert recomputed.messages_token_count > 0 - assert recomputed.total_tokens == ( - recomputed.system_token_count + recomputed.messages_token_count - ) - - result = await session.rpc.history.compact() - assert result is not None - assert result.success, "Expected History.compact() to report success=True" - assert result.messages_removed >= 0, "messages_removed must be non-negative" - if result.context_window is not None: - assert result.context_window.messages_length >= 0 - assert result.context_window.current_tokens >= 0 - - # Session must still be usable after compaction - name = await session.rpc.name.get() - assert name is not None - finally: - await session.disconnect() - - async def test_should_set_and_get_each_session_mode_value(self, ctx: E2ETestContext): - for mode in [SessionMode.INTERACTIVE, SessionMode.PLAN, SessionMode.AUTOPILOT]: - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - await session.rpc.mode.set(ModeSetRequest(mode=mode)) - result = await session.rpc.mode.get() - assert result == mode, f"Expected mode {mode} but got {result}" - finally: - await session.disconnect() - - async def test_should_reject_workspace_file_path_traversal(self, ctx: E2ETestContext): - for traversal_path in [ - "../escaped.txt", - "../../escaped.txt", - "nested/../../../escaped.txt", - ]: - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - with pytest.raises(Exception) as excinfo: - await session.rpc.workspaces.create_file( - WorkspacesCreateFileRequest( - path=traversal_path, - content="should not land outside workspace", - ) - ) - assert "workspace files directory" in str(excinfo.value).lower() - - with pytest.raises(Exception) as excinfo2: - await session.rpc.workspaces.read_file( - WorkspacesReadFileRequest(path=traversal_path) - ) - assert "workspace files directory" in str(excinfo2.value).lower() - finally: - await session.disconnect() - - async def test_should_create_workspace_file_with_nested_path_auto_creating_dirs( - self, ctx: E2ETestContext - ): - import uuid - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - nested_path = f"nested-{uuid.uuid4().hex}/subdir/file.txt" - await session.rpc.workspaces.create_file( - WorkspacesCreateFileRequest(path=nested_path, content="nested content") - ) - read = await session.rpc.workspaces.read_file( - WorkspacesReadFileRequest(path=nested_path) - ) - assert read.content == "nested content" - - listed = await session.rpc.workspaces.list_files() - assert any(f.endswith("file.txt") for f in listed.files) - finally: - await session.disconnect() - - async def test_should_report_error_reading_nonexistent_workspace_file( - self, ctx: E2ETestContext - ): - import uuid - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - with pytest.raises(Exception): - await session.rpc.workspaces.read_file( - WorkspacesReadFileRequest(path=f"never-exists-{uuid.uuid4().hex}.txt") - ) - finally: - await session.disconnect() - - async def test_should_update_existing_workspace_file_with_update_operation( - self, ctx: E2ETestContext - ): - import asyncio - import uuid - - from copilot.session_events import ( - SessionWorkspaceFileChangedData, - WorkspaceFileChangedOperation, - ) - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - path = f"reused-{uuid.uuid4().hex}.txt" - await session.rpc.workspaces.create_file( - WorkspacesCreateFileRequest(path=path, content="v1") - ) - - update_future: asyncio.Future = asyncio.get_event_loop().create_future() - - def on_event(event): - if ( - isinstance(event.data, SessionWorkspaceFileChangedData) - and event.data.path == path - and event.data.operation == WorkspaceFileChangedOperation.UPDATE - and not update_future.done() - ): - update_future.set_result(event) - - unsubscribe = session.on(on_event) - try: - await session.rpc.workspaces.create_file( - WorkspacesCreateFileRequest(path=path, content="v2") - ) - evt = await asyncio.wait_for(update_future, timeout=15.0) - assert evt.data.operation == WorkspaceFileChangedOperation.UPDATE - - read = await session.rpc.workspaces.read_file(WorkspacesReadFileRequest(path=path)) - assert read.content == "v2" - finally: - unsubscribe() - finally: - await session.disconnect() - - async def test_should_reject_empty_or_whitespace_session_name(self, ctx: E2ETestContext): - for empty_name in ["", " ", "\t\n \r"]: - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - with pytest.raises(Exception) as excinfo: - await session.rpc.name.set(NameSetRequest(name=empty_name)) - assert "empty" in str(excinfo.value).lower() - finally: - await session.disconnect() - - async def test_should_emit_title_changed_event_each_time_name_set_is_called( - self, ctx: E2ETestContext - ): - import asyncio - import uuid - - from copilot.session_events import SessionTitleChangedData - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - title_a = f"Title-A-{uuid.uuid4().hex}" - title_b = f"Title-B-{uuid.uuid4().hex}" - - first_task: asyncio.Future = asyncio.get_event_loop().create_future() - second_task: asyncio.Future = asyncio.get_event_loop().create_future() - - def on_event(event): - if isinstance(event.data, SessionTitleChangedData): - if event.data.title == title_a and not first_task.done(): - first_task.set_result(event) - elif event.data.title == title_b and not second_task.done(): - second_task.set_result(event) - - unsubscribe = session.on(on_event) - try: - await session.rpc.name.set(NameSetRequest(name=title_a)) - await asyncio.wait_for(first_task, timeout=15.0) - - await session.rpc.name.set(NameSetRequest(name=title_b)) - second_evt = await asyncio.wait_for(second_task, timeout=15.0) - assert second_evt.data.title == title_b - finally: - unsubscribe() - finally: - await session.disconnect() - - async def test_should_fork_session_to_event_id_excluding_boundary_event( - self, ctx: E2ETestContext - ): - first_prompt = "Say FORK_BOUNDARY_FIRST exactly." - second_prompt = "Say FORK_BOUNDARY_SECOND exactly." - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - await session.send_and_wait(first_prompt, timeout=60.0) - await session.send_and_wait(second_prompt, timeout=60.0) - - source_events = await session.get_events() - second_user_event = next( - ( - e - for e in source_events - if isinstance(e.data, UserMessageData) and e.data.content == second_prompt - ), - None, - ) - assert second_user_event is not None, ( - "Expected the second user.message in persisted history" - ) - boundary_event_id = str(second_user_event.id) - - fork = await ctx.client.rpc.sessions.fork( - SessionsForkRequest(session_id=session.session_id, to_event_id=boundary_event_id) - ) - assert (fork.session_id or "").strip() - assert fork.session_id != session.session_id - - forked_session = await ctx.client.resume_session( - fork.session_id, - on_permission_request=PermissionHandler.approve_all, - ) - try: - forked_events = await forked_session.get_events() - forked_ids = {str(e.id) for e in forked_events} - assert boundary_event_id not in forked_ids, ( - "toEventId is exclusive — boundary event must not be in forked session" - ) - - forked_conv = _conversation_messages(forked_events) - assert any(r == "user" and c == first_prompt for r, c in forked_conv) - assert not any(r == "user" and c == second_prompt for r, c in forked_conv) - finally: - await forked_session.disconnect() - finally: - await session.disconnect() - - async def test_should_report_error_when_forking_session_to_unknown_event_id( - self, ctx: E2ETestContext - ): - import uuid - - source_prompt = "Say FORK_UNKNOWN_EVENT_OK exactly." - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - await session.send_and_wait(source_prompt, timeout=60.0) - - bogus_event_id = str(uuid.uuid4()) - with pytest.raises(Exception) as excinfo: - await ctx.client.rpc.sessions.fork( - SessionsForkRequest(session_id=session.session_id, to_event_id=bogus_event_id) - ) - text = str(excinfo.value) - assert f"Event {bogus_event_id} not found".lower() in text.lower() - assert "Unhandled method sessions.fork".lower() not in text.lower() - finally: - await session.disconnect() diff --git a/python/e2e/test_rpc_session_state_extras_e2e.py b/python/e2e/test_rpc_session_state_extras_e2e.py deleted file mode 100644 index 02ee0cd790..0000000000 --- a/python/e2e/test_rpc_session_state_extras_e2e.py +++ /dev/null @@ -1,303 +0,0 @@ -""" -E2E coverage for additional session-scoped RPC methods. - -Mirrors ``dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs`` (snapshot -category ``rpc_session_state_extras``). -""" - -from __future__ import annotations - -import contextlib -import json -import time - -import pytest - -from copilot import CopilotClient, RuntimeConnection -from copilot.rpc import ( - CompletionsRequestRequest, - MetadataContextHeaviestMessagesRequest, - ModelSwitchToRequest, - NamedProviderConfig, - PermissionsSetModeRequest, - ProviderAddRequest, - ProviderModelConfig, - ProviderType, - ProviderWireAPI, - SessionVisibilityStatus, - SubagentSettings, - SubagentSettingsEntry, - SubagentSettingsEntryContextTier, - UpdateSubagentSettingsRequest, - VisibilitySetRequest, -) -from copilot.session import PermissionHandler -from copilot.session_events import PermissionMode - -from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -def _make_authed_client(ctx: E2ETestContext, token: str) -> CopilotClient: - env = ctx.get_env() - env["COPILOT_DEBUG_GITHUB_API_URL"] = ctx.proxy_url - return CopilotClient( - connection=RuntimeConnection.for_stdio(path=ctx.cli_path), - working_directory=ctx.work_dir, - env=env, - github_token=token, - ) - - -async def _configure_user(ctx: E2ETestContext, token: str) -> None: - await ctx.set_copilot_user_by_token( - token, - { - "login": "rpc-session-extras-user", - "copilot_plan": "individual_pro", - "endpoints": { - "api": ctx.proxy_url, - "telemetry": "https://localhost:1/telemetry", - }, - "analytics_tracking_id": "rpc-session-extras-tracking-id", - }, - ) - - -async def _stop_client(client: CopilotClient) -> None: - with contextlib.suppress(ExceptionGroup): - await client.stop() - - -class TestRpcSessionStateExtras: - async def test_should_list_models_for_session(self, ctx: E2ETestContext): - token = "rpc-session-model-list-token" - await _configure_user(ctx, token) - client = _make_authed_client(ctx, token) - try: - async with await client.create_session( - model="claude-sonnet-5", - on_permission_request=PermissionHandler.approve_all, - github_token=token, - ) as session: - result = await session.rpc.model.list() - - assert result.list is not None - assert len(result.list) > 0 - assert any( - "claude-sonnet-5" in json.dumps(model, sort_keys=True) for model in result.list - ) - finally: - await _stop_client(client) - - async def test_should_report_session_activity_when_idle(self, ctx: E2ETestContext): - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) as session: - activity = await session.rpc.metadata.activity() - - assert activity.has_active_work is False - assert activity.abortable is False - - async def test_should_add_byok_provider_and_model_at_runtime(self, ctx: E2ETestContext): - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) as session: - provider_name = f"sdk-runtime-provider-{time.time_ns()}" - model_id = "sdk-runtime-model" - selection_id = f"{provider_name}/{model_id}" - - added = await session.rpc.provider.add( - ProviderAddRequest( - providers=[ - NamedProviderConfig( - name=provider_name, - type=ProviderType.OPENAI, - wire_api=ProviderWireAPI.COMPLETIONS, - base_url="https://api.example.test/v1", - api_key="runtime-provider-secret", - headers={"X-SDK-Provider": "runtime"}, - ) - ], - models=[ - ProviderModelConfig( - provider=provider_name, - id=model_id, - name="SDK Runtime Model", - model_id="claude-sonnet-5", - wire_model="wire-sdk-runtime-model", - max_context_window_tokens=4096, - max_prompt_tokens=3072, - max_output_tokens=1024, - ) - ], - ) - ) - - assert len(added.models) == 1 - assert selection_id in json.dumps(added.models[0], sort_keys=True) - assert "SDK Runtime Model" in json.dumps(added.models[0], sort_keys=True) - - listed = await session.rpc.model.list() - assert any(selection_id in json.dumps(model, sort_keys=True) for model in listed.list) - - switched = await session.rpc.model.switch_to( - ModelSwitchToRequest(model_id=selection_id) - ) - assert switched.model_id == selection_id - assert (await session.rpc.model.get_current()).model_id == selection_id - - async def test_should_return_empty_completions_when_host_does_not_provide_them( - self, ctx: E2ETestContext - ): - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) as session: - triggers = await session.rpc.completions.get_trigger_characters() - assert triggers.trigger_characters == [] - - completions = await session.rpc.completions.request( - CompletionsRequestRequest(text="Use @", offset=5) - ) - assert completions.items == [] - - async def test_should_report_visibility_as_unsynced_for_local_session( - self, ctx: E2ETestContext - ): - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) as session: - initial = await session.rpc.visibility.get() - assert initial.synced is False - assert initial.status is None - assert initial.share_url is None - - updated = await session.rpc.visibility.set( - VisibilitySetRequest(status=SessionVisibilityStatus.REPO) - ) - assert updated.synced is False - assert updated.status is None - assert updated.share_url is None - - async def test_should_get_and_set_allowall_permissions(self, ctx: E2ETestContext): - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) as session: - try: - initial = await session.rpc.permissions.get_mode() - assert initial.mode is PermissionMode.MANUAL - - enable = await session.rpc.permissions.set_mode( - PermissionsSetModeRequest(mode=PermissionMode.ALLOW_ALL) - ) - assert enable.success is True - assert enable.mode is PermissionMode.ALLOW_ALL - assert (await session.rpc.permissions.get_mode()).mode is PermissionMode.ALLOW_ALL - - disable = await session.rpc.permissions.set_mode( - PermissionsSetModeRequest(mode=PermissionMode.MANUAL) - ) - assert disable.success is True - assert disable.mode is PermissionMode.MANUAL - assert (await session.rpc.permissions.get_mode()).mode is PermissionMode.MANUAL - finally: - with contextlib.suppress(Exception): - await session.rpc.permissions.set_mode( - PermissionsSetModeRequest(mode=PermissionMode.MANUAL) - ) - - async def test_should_get_context_attribution_and_heaviest_messages_after_turn( - self, ctx: E2ETestContext - ): - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) as session: - answer = await session.send_and_wait("Say CONTEXT_METADATA_OK exactly.", timeout=60.0) - assert answer is not None - assert "CONTEXT_METADATA_OK" in (answer.data.content or "") - - attribution = await session.rpc.metadata.get_context_attribution() - assert attribution.context_attribution is not None - context_attribution = attribution.context_attribution - assert context_attribution.total_tokens > 0 - assert len(context_attribution.entries) > 0 - for entry in context_attribution.entries: - assert entry.id.strip() - assert entry.kind.strip() - assert entry.label.strip() - assert entry.tokens >= 0 - for key in entry.attributes or {}: - assert key.strip() - - heaviest = await session.rpc.metadata.get_context_heaviest_messages( - MetadataContextHeaviestMessagesRequest(limit=2) - ) - assert heaviest.total_tokens > 0 - assert len(heaviest.messages) <= 2 - for message in heaviest.messages: - assert message.id.strip() - assert message.tokens >= 0 - - async def test_should_update_and_clear_live_subagent_settings(self, ctx: E2ETestContext): - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) as session: - await session.rpc.tools.update_subagent_settings( - UpdateSubagentSettingsRequest( - subagents=SubagentSettings( - { - "general-purpose": SubagentSettingsEntry( - model="claude-haiku-4.5", - effort_level="low", - context_tier=SubagentSettingsEntryContextTier.DEFAULT, - ) - } - ) - ) - ) - - await session.rpc.tools.update_subagent_settings( - UpdateSubagentSettingsRequest(subagents=None) - ) - - async def test_should_read_empty_sql_todos_for_fresh_session(self, ctx: E2ETestContext): - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) as session: - result = await session.rpc.plan.read_sql_todos() - - assert result.rows is not None - assert result.rows == [] - - async def test_should_get_telemetry_engagement_id(self, ctx: E2ETestContext): - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) as session: - result = await session.rpc.telemetry.get_engagement_id() - - assert result is not None - - async def test_should_get_current_tool_metadata_after_initialization(self, ctx: E2ETestContext): - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) as session: - answer = await session.send_and_wait("What is 2+2?", timeout=60.0) - assert answer is not None - - result = await session.rpc.tools.get_current_metadata() - - assert result.tools is not None - assert len(result.tools) > 0 - assert all((tool.name or "").strip() for tool in result.tools) - assert all(tool.description is not None for tool in result.tools) - - async def test_should_reload_session_plugins(self, ctx: E2ETestContext): - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) as session: - await session.rpc.plugins.reload() - - plugins = await session.rpc.plugins.list() - assert plugins.plugins is not None - assert all((plugin.name or "").strip() for plugin in plugins.plugins) diff --git a/python/e2e/test_rpc_shell_and_fleet_e2e.py b/python/e2e/test_rpc_shell_and_fleet_e2e.py deleted file mode 100644 index 422feab520..0000000000 --- a/python/e2e/test_rpc_shell_and_fleet_e2e.py +++ /dev/null @@ -1,172 +0,0 @@ -""" -E2E coverage for ``session.shell.*`` and ``session.fleet.*`` RPCs. - -Mirrors ``dotnet/test/RpcShellAndFleetTests.cs`` (snapshot category -``rpc_shell_and_fleet``). -""" - -from __future__ import annotations - -import asyncio -import sys -import tempfile -import uuid -from pathlib import Path - -import pytest - -from copilot.rpc import ( - FleetStartRequest, - ShellExecRequest, - ShellKillRequest, -) -from copilot.session import PermissionHandler -from copilot.session_events import ( - AssistantMessageData, - SessionErrorData, - ToolExecutionCompleteData, - ToolExecutionStartData, - UserMessageData, -) -from copilot.tools import Tool, ToolInvocation, ToolResult - -from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -def _write_file_command(marker_path: Path, marker: str) -> str: - if sys.platform == "win32": - # shell.exec already runs through cmd.exe on Windows. Use its built-in echo - # instead of spawning a nested PowerShell process just to write the marker. - return f'echo {marker}>"{marker_path.name}"' - return f"sh -c \"printf '%s' '{marker}' > '{marker_path.name}'\"" - - -async def _wait_for_file_text(path: Path, expected: str, *, timeout: float = 30.0) -> None: - deadline = asyncio.get_event_loop().time() + timeout - while asyncio.get_event_loop().time() < deadline: - if path.exists(): - text = path.read_text(encoding="utf-8") - if expected in text: - return - await asyncio.sleep(0.1) - raise TimeoutError(f"Timed out waiting for shell command to write '{expected}' to '{path}'.") - - -class TestRpcShellAndFleet: - async def test_should_execute_shell_command(self, ctx: E2ETestContext): - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) as session: - command_dir = Path(ctx.work_dir) / f"shell-rpc-{uuid.uuid4().hex}" - command_dir.mkdir() - marker_path = command_dir / "marker.txt" - marker = "copilot-sdk-shell-rpc" - - result = await session.rpc.shell.exec( - ShellExecRequest( - command=_write_file_command(marker_path, marker), cwd=str(command_dir) - ) - ) - assert (result.process_id or "").strip() - await _wait_for_file_text(marker_path, marker) - - async def test_should_kill_shell_process(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - if sys.platform == "win32": - command = 'powershell -NoLogo -NoProfile -Command "Start-Sleep -Seconds 30"' - else: - command = "sleep 30" - - # On Windows, terminating the shell wrapper can briefly leave grandchildren alive. - # Keep this command outside the fixture workspace so cleanup is not blocked by cwd handles. - exec_result = await session.rpc.shell.exec( - ShellExecRequest(command=command, cwd=tempfile.gettempdir()) - ) - assert (exec_result.process_id or "").strip() - - kill_result = await session.rpc.shell.kill( - ShellKillRequest(process_id=exec_result.process_id) - ) - assert kill_result.killed - - await session.disconnect() - - async def test_should_start_fleet_and_complete_custom_tool_task(self, ctx: E2ETestContext): - marker_path = Path(ctx.work_dir) / f"fleet-rpc-{uuid.uuid4().hex}.txt" - marker = "copilot-sdk-fleet-rpc" - tool_name = "record_fleet_completion" - - def record_fleet_completion(invocation: ToolInvocation) -> ToolResult: - args = invocation.arguments or {} - content = str(args.get("content", "")) - marker_path.write_text(content, encoding="utf-8") - return ToolResult(text_result_for_llm=content) - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - tools=[ - Tool( - name=tool_name, - description="Records completion of the fleet validation task.", - parameters={ - "type": "object", - "properties": {"content": {"type": "string", "description": "Marker"}}, - "required": ["content"], - }, - handler=record_fleet_completion, - ) - ], - ) - - prompt = ( - f"Use the {tool_name} tool with content '{marker}', " - "then report that the fleet task is complete." - ) - result = await session.rpc.fleet.start(FleetStartRequest(prompt=prompt)) - assert result.started - await _wait_for_file_text(marker_path, marker) - - async def _wait_for_messages(timeout: float = 120.0): - deadline = asyncio.get_event_loop().time() + timeout - while asyncio.get_event_loop().time() < deadline: - messages = await session.get_events() - if any( - isinstance(m.data, AssistantMessageData) - and "fleet task" in (m.data.content or "").lower() - for m in messages - ): - return messages - if any(isinstance(m.data, SessionErrorData) for m in messages): - raise RuntimeError("Session error while waiting for fleet completion") - await asyncio.sleep(0.25) - raise TimeoutError("Timed out waiting for fleet-mode assistant reply.") - - messages = await _wait_for_messages() - assert any( - isinstance(m.data, UserMessageData) and prompt in (m.data.content or "") - for m in messages - ) - assert any( - isinstance(m.data, ToolExecutionStartData) and m.data.tool_name == tool_name - for m in messages - ) - assert any( - isinstance(m.data, ToolExecutionCompleteData) - and m.data.success - and ( - getattr(m.data, "result", None) is not None - and marker in (m.data.result.content or "") - ) - for m in messages - ) - assert any( - isinstance(m.data, AssistantMessageData) - and "fleet task" in (m.data.content or "").lower() - for m in messages - ) - - await session.disconnect() diff --git a/python/e2e/test_rpc_shell_user_requested_e2e.py b/python/e2e/test_rpc_shell_user_requested_e2e.py deleted file mode 100644 index 11775e5fc4..0000000000 --- a/python/e2e/test_rpc_shell_user_requested_e2e.py +++ /dev/null @@ -1,122 +0,0 @@ -""" -E2E coverage for session-scoped user-requested shell RPC methods. - -Mirrors ``dotnet/test/E2E/RpcShellUserRequestedE2ETests.cs`` (snapshot -category ``rpc_shell_user_requested``). -""" - -from __future__ import annotations - -import asyncio -import contextlib -import sys -import uuid -from pathlib import Path - -import pytest - -from copilot.rpc import ShellCancelUserRequestedRequest, ShellExecuteUserRequestedRequest -from copilot.session import PermissionHandler - -from .testharness import E2ETestContext, wait_for_condition - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -def _create_marker_then_sleep_command(marker_path: Path, seconds: int) -> str: - if sys.platform == "win32": - return ( - f"Set-Content -LiteralPath '{marker_path}' -Value 'running'; " - f"Start-Sleep -Seconds {seconds}" - ) - return f"printf '%s' running > '{marker_path}'; sleep {seconds}" - - -class TestRpcShellUserRequested: - async def test_should_execute_user_requested_shell_command(self, ctx: E2ETestContext): - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) as session: - marker = f"copilotusershell{uuid.uuid4().hex}" - request_id = f"req-{uuid.uuid4().hex}" - - result = await session.rpc.shell.execute_user_requested( - ShellExecuteUserRequestedRequest(command=f"echo {marker}", request_id=request_id) - ) - - assert result.success is True, f"Expected success. Error: {result.error}" - assert result.exit_code == 0 - assert marker in result.output - assert (result.tool_call_id or "").strip() - - async def test_should_cancel_user_requested_shell_command(self, ctx: E2ETestContext): - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) as session: - missing = await session.rpc.shell.cancel_user_requested( - ShellCancelUserRequestedRequest(request_id=f"missing-{uuid.uuid4().hex}") - ) - assert missing.cancelled is False - - request_id = f"req-{uuid.uuid4().hex}" - marker_path = Path(ctx.home_dir) / f"shell-cancel-{uuid.uuid4().hex}.txt" - execute_task = asyncio.create_task( - session.rpc.shell.execute_user_requested( - ShellExecuteUserRequestedRequest( - request_id=request_id, - command=_create_marker_then_sleep_command(marker_path, seconds=60), - ) - ) - ) - - try: - await wait_for_condition( - marker_path.exists, - timeout=30.0, - poll_interval=0.1, - timeout_message=( - f"Timed out waiting for the shell command to create '{marker_path}'." - ), - ) - - async def cancel_took_effect() -> bool: - result = await session.rpc.shell.cancel_user_requested( - ShellCancelUserRequestedRequest(request_id=request_id) - ) - return result.cancelled - - await wait_for_condition( - cancel_took_effect, - timeout=15.0, - poll_interval=0.1, - timeout_message=( - "Timed out waiting for the user-requested shell command " - "to become cancellable." - ), - ) - - await wait_for_condition( - execute_task.done, - timeout=30.0, - poll_interval=0.1, - timeout_message="Timed out waiting for cancelled shell command to finish.", - ) - result = await execute_task - assert result.success is False - finally: - if not execute_task.done(): - with contextlib.suppress(Exception): - await session.rpc.shell.cancel_user_requested( - ShellCancelUserRequestedRequest(request_id=request_id) - ) - with contextlib.suppress(Exception): - await wait_for_condition( - execute_task.done, - timeout=30.0, - poll_interval=0.1, - timeout_message="Timed out draining shell command task.", - ) - if not execute_task.done(): - execute_task.cancel() - with contextlib.suppress(OSError): - marker_path.unlink(missing_ok=True) diff --git a/python/e2e/test_rpc_smoke_e2e.py b/python/e2e/test_rpc_smoke_e2e.py new file mode 100644 index 0000000000..f6a731d00a --- /dev/null +++ b/python/e2e/test_rpc_smoke_e2e.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + +"""Representative generated RPC round trips over the real SDK transport.""" + +import pytest + +from copilot import CopilotClient, RuntimeConnection +from copilot.rpc import PingRequest +from copilot.session import PermissionHandler +from copilot.session_events import SessionMode + +from .testharness import CLI_PATH + +pytestmark = pytest.mark.asyncio(loop_scope="module") + + +async def test_generated_rpc_round_trip(): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + try: + await client.start() + + pong = await client.rpc.ping(PingRequest(message="typed rpc test")) + assert pong.message == "pong: typed rpc test" + assert pong.timestamp is not None + + session = await client.create_session(on_permission_request=PermissionHandler.approve_all) + try: + assert await session.rpc.mode.get() == SessionMode.INTERACTIVE + finally: + await session.disconnect() + finally: + await client.force_stop() diff --git a/python/e2e/test_rpc_tasks_and_handlers_e2e.py b/python/e2e/test_rpc_tasks_and_handlers_e2e.py deleted file mode 100644 index f0dd8f7577..0000000000 --- a/python/e2e/test_rpc_tasks_and_handlers_e2e.py +++ /dev/null @@ -1,472 +0,0 @@ -""" -E2E coverage for ``session.tasks.*`` and pending-handler RPCs. - -Mirrors ``dotnet/test/RpcTasksAndHandlersTests.cs`` (snapshot category -``rpc_tasks_and_handlers``). -""" - -from __future__ import annotations - -import asyncio - -import pytest - -from copilot.rpc import ( - CommandsHandlePendingCommandRequest, - HandlePendingToolCallRequest, - MCPHeadersHandlePendingHeadersRefreshRequest, - MCPHeadersHandlePendingHeadersRefreshRequestKind, - MCPHeadersHandlePendingHeadersRefreshRequestRequest, - PermissionDecisionApproveForLocation, - PermissionDecisionApproveForLocationApprovalCustomTool, - PermissionDecisionApproveForSession, - PermissionDecisionApproveForSessionApprovalCustomTool, - PermissionDecisionApprovePermanently, - PermissionDecisionReject, - PermissionDecisionRequest, - TasksCancelRequest, - TasksGetProgressRequest, - TasksPromoteToBackgroundRequest, - TasksRemoveRequest, - TasksSendMessageRequest, - TasksStartAgentRequest, - UIAutoModeSwitchResponse, - UIElicitationRequest, - UIElicitationResponse, - UIElicitationResponseAction, - UIElicitationSchema, - UIElicitationSchemaProperty, - UIElicitationSchemaPropertyType, - UIElicitationSchemaType, - UIExitPlanModeAction, - UIExitPlanModeResponse, - UIHandlePendingAutoModeSwitchRequest, - UIHandlePendingElicitationRequest, - UIHandlePendingExitPlanModeRequest, - UIHandlePendingSamplingRequest, - UIHandlePendingSessionLimitsExhaustedRequest, - UIHandlePendingUserInputRequest, - UISessionLimitsExhaustedResponse, - UISessionLimitsExhaustedResponseAction, - UIUnregisterDirectAutoModeSwitchHandlerRequest, - UIUserInputResponse, -) -from copilot.session import PermissionHandler -from copilot.session_events import ( - AssistantMessageData, - SessionErrorData, -) - -from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -async def _find_agent_task(session, task_id: str): - task_list = await session.rpc.tasks.list() - return next((t for t in (task_list.tasks or []) if t.id == task_id), None) - - -async def _wait_for_agent_task(session, task_id: str, predicate, timeout: float, message: str): - deadline = asyncio.get_running_loop().time() + timeout - last_task = None - while True: - last_task = await _find_agent_task(session, task_id) - if predicate(last_task): - return last_task - if asyncio.get_running_loop().time() >= deadline: - pytest.fail(f"{message}; last observed task: {last_task!r}") - await asyncio.sleep(0.25) - - -async def _assert_implemented_failure(awaitable, method: str) -> None: - with pytest.raises(Exception) as excinfo: - _ = await awaitable - assert f"Unhandled method {method}".lower() not in str(excinfo.value).lower() - - -class TestRpcTasksAndHandlers: - async def test_should_list_task_state_and_return_false_for_missing_task_operations( - self, ctx: E2ETestContext - ): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - tasks = await session.rpc.tasks.list() - assert tasks.tasks is not None - assert len(tasks.tasks) == 0 - - promote = await session.rpc.tasks.promote_to_background( - TasksPromoteToBackgroundRequest(id="missing-task") - ) - assert promote.promoted is False - - cancel = await session.rpc.tasks.cancel(TasksCancelRequest(id="missing-task")) - assert cancel.cancelled is False - - remove = await session.rpc.tasks.remove(TasksRemoveRequest(id="missing-task")) - assert remove.removed is False - - refresh = await session.rpc.tasks.refresh() - assert refresh is not None - - wait = await session.rpc.tasks.wait_for_pending() - assert wait is not None - - progress = await session.rpc.tasks.get_progress( - TasksGetProgressRequest(id="missing-task") - ) - assert progress.progress is None - - promotable = await session.rpc.tasks.get_current_promotable() - assert promotable.task is None - - promote_current = await session.rpc.tasks.promote_current_to_background() - assert promote_current.task is None - - send = await session.rpc.tasks.send_message( - TasksSendMessageRequest(id="missing-task", message="hello") - ) - assert send.sent is False - assert send.error - finally: - await session.disconnect() - - async def test_should_report_implemented_error_for_missing_task_agent_type( - self, ctx: E2ETestContext - ): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - await _assert_implemented_failure( - session.rpc.tasks.start_agent( - TasksStartAgentRequest( - agent_type="missing-agent-type", - prompt="Say hi", - name="sdk-test-task", - ) - ), - "session.tasks.startAgent", - ) - finally: - await session.disconnect() - - async def test_should_return_expected_results_for_missing_pending_handler_request_ids( - self, ctx: E2ETestContext - ): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - tool = await session.rpc.tools.handle_pending_tool_call( - HandlePendingToolCallRequest( - request_id="missing-tool-request", - result="tool result", - ) - ) - assert tool.success is False - - command = await session.rpc.commands.handle_pending_command( - CommandsHandlePendingCommandRequest( - request_id="missing-command-request", - error="command error", - ) - ) - assert command.success is True - - elicitation = await session.rpc.ui.handle_pending_elicitation( - UIHandlePendingElicitationRequest( - request_id="missing-elicitation-request", - result=UIElicitationResponse(action=UIElicitationResponseAction.CANCEL), - ) - ) - assert elicitation.success is False - - user_input = await session.rpc.ui.handle_pending_user_input( - UIHandlePendingUserInputRequest( - request_id="missing-user-input-request", - response=UIUserInputResponse(answer="answer", was_freeform=True), - ) - ) - assert user_input.success is False - - sampling = await session.rpc.ui.handle_pending_sampling( - UIHandlePendingSamplingRequest( - request_id="missing-sampling-request", - response={"role": "assistant", "content": {"type": "text", "text": "hi"}}, - ) - ) - assert sampling.success is False - - auto_mode = await session.rpc.ui.handle_pending_auto_mode_switch( - UIHandlePendingAutoModeSwitchRequest( - request_id="missing-auto-mode-request", - response=UIAutoModeSwitchResponse.NO, - ) - ) - assert auto_mode.success is False - - exit_plan = await session.rpc.ui.handle_pending_exit_plan_mode( - UIHandlePendingExitPlanModeRequest( - request_id="missing-exit-plan-request", - response=UIExitPlanModeResponse( - approved=True, - selected_action=UIExitPlanModeAction.INTERACTIVE, - ), - ) - ) - assert exit_plan.success is False - - permission = await session.rpc.permissions.handle_pending_permission_request( - PermissionDecisionRequest( - request_id="missing-permission-request", - result=PermissionDecisionReject(feedback="not approved"), - ) - ) - assert permission.success is False - - permanent = await session.rpc.permissions.handle_pending_permission_request( - PermissionDecisionRequest( - request_id="missing-permanent-permission-request", - result=PermissionDecisionApprovePermanently(domain="example.com"), - ) - ) - assert permanent.success is False - - session_approval = await session.rpc.permissions.handle_pending_permission_request( - PermissionDecisionRequest( - request_id="missing-session-approval-request", - result=PermissionDecisionApproveForSession( - approval=PermissionDecisionApproveForSessionApprovalCustomTool( - tool_name="missing-tool", - ), - ), - ) - ) - assert session_approval.success is False - - location_approval = await session.rpc.permissions.handle_pending_permission_request( - PermissionDecisionRequest( - request_id="missing-location-approval-request", - result=PermissionDecisionApproveForLocation( - location_key="missing-location", - approval=PermissionDecisionApproveForLocationApprovalCustomTool( - tool_name="missing-tool", - ), - ), - ) - ) - assert location_approval.success is False - - session_limits = await session.rpc.ui.handle_pending_session_limits_exhausted( - UIHandlePendingSessionLimitsExhaustedRequest( - request_id="missing-session-limits-request", - response=UISessionLimitsExhaustedResponse( - action=UISessionLimitsExhaustedResponseAction.CANCEL - ), - ) - ) - assert session_limits.success is False - - headers = await session.rpc.mcp.headers.handle_pending_headers_refresh_request( - MCPHeadersHandlePendingHeadersRefreshRequestRequest( - request_id="missing-headers-refresh-request", - result=MCPHeadersHandlePendingHeadersRefreshRequest( - kind=MCPHeadersHandlePendingHeadersRefreshRequestKind.HEADERS, - headers={"X-SDK-Test": "missing"}, - ), - ) - ) - assert headers.success is False - - no_headers = await session.rpc.mcp.headers.handle_pending_headers_refresh_request( - MCPHeadersHandlePendingHeadersRefreshRequestRequest( - request_id="missing-headers-refresh-none-request", - result=MCPHeadersHandlePendingHeadersRefreshRequest( - kind=MCPHeadersHandlePendingHeadersRefreshRequestKind.NONE, - ), - ) - ) - assert no_headers.success is False - finally: - await session.disconnect() - - async def test_should_round_trip_rpc_ui_elicitation_and_direct_auto_mode_switch( - self, ctx: E2ETestContext - ): - seen_contexts = [] - - async def on_elicitation(context): - seen_contexts.append(context) - assert context["message"] == "Choose deployment" - schema = context["requestedSchema"] - assert schema["properties"]["environment"]["enum"] == ["staging", "production"] - return {"action": "accept", "content": {"environment": "staging"}} - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - on_elicitation_request=on_elicitation, - ) - try: - response = await session.rpc.ui.elicitation( - UIElicitationRequest( - message="Choose deployment", - requested_schema=UIElicitationSchema( - type=UIElicitationSchemaType.OBJECT, - required=["environment"], - properties={ - "environment": UIElicitationSchemaProperty( - type=UIElicitationSchemaPropertyType.STRING, - enum=["staging", "production"], - ) - }, - ), - ) - ) - assert response.action == UIElicitationResponseAction.ACCEPT - assert response.content == {"environment": "staging"} - assert len(seen_contexts) == 1 - - registered = await session.rpc.ui.register_direct_auto_mode_switch_handler() - assert registered.handle - - unregistered = await session.rpc.ui.unregister_direct_auto_mode_switch_handler( - UIUnregisterDirectAutoModeSwitchHandlerRequest(handle=registered.handle) - ) - assert unregistered.unregistered is True - - unregistered_again = await session.rpc.ui.unregister_direct_auto_mode_switch_handler( - UIUnregisterDirectAutoModeSwitchHandlerRequest(handle=registered.handle) - ) - assert unregistered_again.unregistered is False - finally: - await session.disconnect() - - async def test_should_report_implemented_error_for_invalid_task_agent_model( - self, ctx: E2ETestContext - ): - """Invalid model name for agent task returns an error without 'Unhandled method'.""" - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - with pytest.raises(Exception) as excinfo: - await session.rpc.tasks.start_agent( - TasksStartAgentRequest( - agent_type="general-purpose", - prompt="Say hi", - name="sdk-test-invalid-model", - model="not-a-real-model", - ) - ) - text = str(excinfo.value).lower() - assert "unhandled method session.tasks.startagent" not in text - - tasks = await session.rpc.tasks.list() - assert tasks.tasks is not None - assert len(tasks.tasks) == 0, "Task list should be empty after invalid start" - finally: - await session.disconnect() - - async def test_should_start_background_agent_and_report_task_details(self, ctx: E2ETestContext): - """Start a background agent task and verify task details then remove it.""" - from copilot.rpc import ( - TaskAgentInfo, - TaskInfoExecutionMode, - TaskInfoStatus, - ) - - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - task_completion_notification = asyncio.get_running_loop().create_future() - - def on_event(event): - if isinstance(event.data, AssistantMessageData) and "TASK_AGENT_DONE" in ( - event.data.content or "" - ): - if not task_completion_notification.done(): - task_completion_notification.set_result(event) - elif isinstance(event.data, SessionErrorData): - if not task_completion_notification.done(): - task_completion_notification.set_exception( - RuntimeError(event.data.message or "session error") - ) - - unsubscribe = session.on(on_event) - try: - ready = await session.send_and_wait( - "Reply with TASK_AGENT_READY exactly.", - timeout=60.0, - ) - assert ready is not None - assert "TASK_AGENT_READY" in (ready.data.content or "") - - start_result = await session.rpc.tasks.start_agent( - TasksStartAgentRequest( - agent_type="general-purpose", - prompt="Reply with TASK_AGENT_DONE exactly.", - name="sdk-background-agent", - description="SDK background agent coverage", - ) - ) - task_id = start_result.agent_id - assert task_id, "Expected a task ID from start_agent" - - found_task = await _wait_for_agent_task( - session, - task_id, - lambda task: task is not None, - 30.0, - f"Task {task_id} not found in tasks list", - ) - assert found_task.id == task_id - assert found_task.description == "SDK background agent coverage" - assert isinstance(found_task, TaskAgentInfo) - assert found_task.agent_type == "general-purpose" - assert found_task.execution_mode == TaskInfoExecutionMode.BACKGROUND - assert found_task.prompt == "Reply with TASK_AGENT_DONE exactly." - - found_task = await _wait_for_agent_task( - session, - task_id, - lambda task: ( - task is None - or task.status - in ( - TaskInfoStatus.COMPLETED, - TaskInfoStatus.FAILED, - TaskInfoStatus.CANCELLED, - TaskInfoStatus.IDLE, - ) - ), - 60.0, - f"Task {task_id} did not produce a final observable state", - ) - if found_task is not None: - assert "TASK_AGENT_DONE" in (found_task.latest_response or found_task.result or "") - - if found_task.status == TaskInfoStatus.IDLE: - cancel = await session.rpc.tasks.cancel(TasksCancelRequest(id=task_id)) - assert cancel.cancelled is True - - remove = await session.rpc.tasks.remove(TasksRemoveRequest(id=task_id)) - # Completion delivery also removes finished tasks, so this call may lose that race. - assert remove.removed or task_completion_notification.done(), ( - f"Task {task_id} was not removed before its completion " - "notification was delivered" - ) - - after_remove = await session.rpc.tasks.list() - task_after_remove = next( - (task for task in (after_remove.tasks or []) if task.id == task_id), - None, - ) - assert task_after_remove is None - - await asyncio.wait_for(task_completion_notification, timeout=30.0) - finally: - unsubscribe() - await session.disconnect() diff --git a/python/e2e/test_rpc_ui_ephemeral_query_e2e.py b/python/e2e/test_rpc_ui_ephemeral_query_e2e.py deleted file mode 100644 index b7f3b94d22..0000000000 --- a/python/e2e/test_rpc_ui_ephemeral_query_e2e.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -E2E coverage for session-scoped UI ephemeral query RPC. - -Mirrors ``dotnet/test/E2E/RpcUiEphemeralQueryE2ETests.cs`` (snapshot -category ``rpc_ui_ephemeral_query``). -""" - -from __future__ import annotations - -import pytest - -from copilot.rpc import UIEphemeralQueryRequest -from copilot.session import PermissionHandler - -from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -class TestRpcUiEphemeralQuery: - # TODO(cli-1.0.81-2): CLI 1.0.81-5 still fails session.ui.ephemeralQuery against the - # recorded snapshot on macOS ("Failed to get response from the AI model"). Re-enable - # once the runtime fix ships. - @pytest.mark.skip( - reason="blocked on CLI 1.0.81-5 session.ui.ephemeralQuery regression on macOS" - ) - async def test_should_answer_ephemeral_query(self, ctx: E2ETestContext): - async with await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) as session: - result = await session.rpc.ui.ephemeral_query( - UIEphemeralQueryRequest( - question="In one word, what is the primary color of a clear daytime sky?" - ) - ) - - assert result is not None - assert (result.answer or "").strip() - assert "blue" in result.answer.lower() diff --git a/python/e2e/test_rpc_workspace_checkpoints_e2e.py b/python/e2e/test_rpc_workspace_checkpoints_e2e.py deleted file mode 100644 index a4ad9cf7ee..0000000000 --- a/python/e2e/test_rpc_workspace_checkpoints_e2e.py +++ /dev/null @@ -1,133 +0,0 @@ -"""E2E coverage for workspace checkpoint, diff, and large-paste RPCs.""" - -from __future__ import annotations - -import subprocess -import uuid -from pathlib import Path - -import pytest - -from copilot.rpc import ( - WorkspaceDiffFileChangeType, - WorkspaceDiffMode, - WorkspacesDiffRequest, - WorkspacesReadCheckpointRequest, - WorkspacesReadFileRequest, - WorkspacesSaveLargePasteRequest, -) -from copilot.session import PermissionHandler - -from .testharness import E2ETestContext - -pytestmark = pytest.mark.asyncio(loop_scope="module") - - -def _run_git(repo: Path, *args: str) -> None: - subprocess.run( - ["git", *args], - cwd=repo, - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - - -def _create_repo_with_unstaged_changes(work_dir: str) -> Path: - repo = Path(work_dir) / f"workspace-diff-{uuid.uuid4().hex}" - repo.mkdir(parents=True) - _run_git(repo, "init") - _run_git(repo, "config", "user.email", "copilot-sdk-e2e@example.com") - _run_git(repo, "config", "user.name", "Copilot SDK E2E") - - (repo / "tracked.txt").write_text("before\n", encoding="utf-8", newline="\n") - (repo / "removed.txt").write_text("remove me\n", encoding="utf-8", newline="\n") - _run_git(repo, "add", "tracked.txt", "removed.txt") - _run_git(repo, "commit", "-m", "initial") - - (repo / "tracked.txt").write_text("after\n", encoding="utf-8", newline="\n") - (repo / "removed.txt").unlink() - return repo - - -class TestRpcWorkspaceCheckpoints: - async def test_should_list_no_checkpoints_for_fresh_session(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - result = await session.rpc.workspaces.list_checkpoints() - assert result.checkpoints == [] - finally: - await session.disconnect() - - async def test_should_return_null_or_empty_content_for_unknown_checkpoint( - self, ctx: E2ETestContext - ): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - result = await session.rpc.workspaces.read_checkpoint( - WorkspacesReadCheckpointRequest(number=2_147_483_647) - ) - assert not result.content - finally: - await session.disconnect() - - async def test_should_return_typed_workspace_diff_result_for_real_changes( - self, ctx: E2ETestContext - ): - repo = _create_repo_with_unstaged_changes(ctx.work_dir) - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - working_directory=str(repo), - ) - try: - result = await session.rpc.workspaces.diff( - WorkspacesDiffRequest(mode=WorkspaceDiffMode.UNSTAGED) - ) - - assert result.requested_mode == WorkspaceDiffMode.UNSTAGED - assert result.mode in (WorkspaceDiffMode.UNSTAGED, WorkspaceDiffMode.BRANCH) - by_path = {change.path.replace("\\", "/"): change for change in result.changes} - - tracked = by_path.get("tracked.txt") - assert tracked is not None - assert tracked.change_type == WorkspaceDiffFileChangeType.MODIFIED - assert "after" in tracked.diff - - removed = by_path.get("removed.txt") - assert removed is not None - assert removed.change_type == WorkspaceDiffFileChangeType.DELETED - assert "remove me" in removed.diff - finally: - await session.disconnect() - - async def test_should_save_large_paste_and_expose_readable_content(self, ctx: E2ETestContext): - session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, - ) - try: - content = "Large paste payload 🚀\n" * 512 - result = await session.rpc.workspaces.save_large_paste( - WorkspacesSaveLargePasteRequest(content=content) - ) - saved = result.saved - - assert saved is not None - assert saved.filename - assert saved.file_path - assert saved.size_bytes == len(content.encode("utf-8")) - - try: - read = await session.rpc.workspaces.read_file( - WorkspacesReadFileRequest(path=saved.filename) - ) - except Exception: - assert Path(saved.file_path).exists() - assert Path(saved.file_path).read_text(encoding="utf-8") == content - else: - assert read.content == content - finally: - await session.disconnect() diff --git a/python/e2e/test_runtime_host_e2e.py b/python/e2e/test_runtime_host_e2e.py index c597e7aab3..5300f8aacb 100644 --- a/python/e2e/test_runtime_host_e2e.py +++ b/python/e2e/test_runtime_host_e2e.py @@ -112,7 +112,10 @@ def exited(self, event) -> None: self.exit_event.set() def options(self) -> AhpHostOptions: + from copilot.rpc import HostLocalServerOptions + return AhpHostOptions( + local_server=HostLocalServerOptions(), create_session=self.create, resume_session=self.resume, on_session_released=self.release, @@ -122,6 +125,8 @@ def options(self) -> AhpHostOptions: @asynccontextmanager async def connect(ahp: AhpTestClient, host: AhpHost, client_id: str | None = None): + assert host.url is not None + assert host.environment_id is None command = { "op": "connect", "url": host.url, diff --git a/python/e2e/test_session_e2e.py b/python/e2e/test_session_e2e.py index 7934ae52f0..21bb8bd3a4 100644 --- a/python/e2e/test_session_e2e.py +++ b/python/e2e/test_session_e2e.py @@ -1003,15 +1003,23 @@ def on_event(event): events.append(event) unsubscribe = session.on(on_event) - idle_task = get_next_event_of_type(session, "session.idle", timeout=10.0) + idle_task = get_next_event_of_type(session, "session.idle") + started = asyncio.get_running_loop().time() try: # Use a slow command so we can verify send() returns before completion await session.send("Run 'sleep 2 && echo done'") + send_duration = asyncio.get_running_loop().time() - started # send() should return before turn completes (no session.idle yet) assert not any(event.type.value == "session.idle" for event in events) - await idle_task + try: + await idle_task + except TimeoutError as exc: + raise AssertionError( + f"session.idle did not arrive after send() returned in {send_duration:.1f}s; " + f"observed events: {[event.type.value for event in events]}" + ) from exc messages = [event for event in events if event.type.value == "assistant.message"] assert messages assert "done" in messages[-1].data.content diff --git a/python/e2e/test_session_fs_e2e.py b/python/e2e/test_session_fs_e2e.py index 035db125ba..73104db314 100644 --- a/python/e2e/test_session_fs_e2e.py +++ b/python/e2e/test_session_fs_e2e.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +import base64 import datetime as dt import os import re @@ -27,7 +28,10 @@ SessionCompactionCompleteData, SessionEvent, ) -from copilot.session_fs_provider import SessionFsFileInfo, SessionFsProvider +from copilot.session_fs_provider import ( + SessionFsFileInfo, + SessionFsProvider, +) from .testharness import DEFAULT_GITHUB_TOKEN, E2ETestContext @@ -66,6 +70,51 @@ async def session_fs_client(ctx: E2ETestContext): class TestSessionFs: + async def test_should_view_an_image_that_exists_only_in_the_binary_session_fs_provider( + self, ctx: E2ETestContext + ): + image_path = "/sdk-provider-image.png" + image_bytes = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==" + ) + provider_root = Path(ctx.work_dir) / "binary-provider" + read_paths: list[str] = [] + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=DEFAULT_GITHUB_TOKEN, + session_fs={**SESSION_FS_CONFIG, "capabilities": {"binary": True}}, + ) + try: + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + create_session_fs_handler=lambda session: _BinaryTestSessionFsProvider( + provider_root, session.session_id, read_paths + ), + ) + stored_path = provider_path(provider_root, session.session_id, image_path) + stored_path.parent.mkdir(parents=True, exist_ok=True) + stored_path.write_bytes(image_bytes) + assert not Path(image_path).exists() + + msg = await session.send_and_wait( + "Use the view tool to view /sdk-provider-image.png, " + "then reply with exactly SDK_PROVIDER_IMAGE_DONE." + ) + assert msg is not None and "SDK_PROVIDER_IMAGE_DONE" in (msg.data.content or "") + assert image_path in read_paths + events = await session.get_events() + assert any( + event.type.value == "session.binary_asset" + and event.data.mime_type == "image/png" + and event.data.data == base64.b64encode(image_bytes).decode("ascii") + for event in events + ) + await session.disconnect() + finally: + await client.stop() + async def test_should_route_file_operations_through_the_session_fs_provider( self, ctx: E2ETestContext, session_fs_client: CopilotClient ): @@ -638,6 +687,21 @@ async def rename(self, src: str, dest: str) -> None: self._path(src).replace(d) +class _BinaryTestSessionFsProvider(_TestSessionFsProvider): + def __init__(self, provider_root: Path, session_id: str, read_paths: list[str]): + super().__init__(provider_root, session_id) + self._read_paths = read_paths + + async def read_file_bytes(self, path: str) -> bytes: + self._read_paths.append(path) + return self._path(path).read_bytes() + + async def write_file_bytes(self, path: str, content: bytes, mode: int | None = None) -> None: + target = self._path(path) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(content) + + def create_test_session_fs_handler(provider_root: Path): def create_handler(session): return _TestSessionFsProvider(provider_root, session.session_id) diff --git a/python/e2e/test_set_tools_e2e.py b/python/e2e/test_set_tools_e2e.py new file mode 100644 index 0000000000..29bc6f0812 --- /dev/null +++ b/python/e2e/test_set_tools_e2e.py @@ -0,0 +1,195 @@ +"""Live tool replacement E2Es using shared set_tools provider captures.""" + +import json + +import pytest +from pydantic import BaseModel, Field + +from copilot import define_tool +from copilot.session import PermissionHandler +from copilot.tools import Tool, ToolInvocation, ToolResult + +from .testharness import E2ETestContext + +pytestmark = pytest.mark.asyncio(loop_scope="module") + +FRUIT_PROMPT = "Use lookup_fruit to find the fruit for code 42." +FRUIT_AND_VEGETABLE_PROMPT = ( + "Use lookup_fruit to find the fruit for code 42 again, and use lookup_vegetable to find the " + "vegetable for code 7." +) +VEGETABLE_PROMPT = "Use lookup_vegetable to find the vegetable for code 7." + + +class FruitParams(BaseModel): + code: int = Field(description="Fruit code") + + +class VegetableParams(BaseModel): + code: int = Field(description="Vegetable code") + + +def lookup_fruit(fruit: str, calls: list[int] | None = None) -> Tool: + @define_tool("lookup_fruit", description="Looks up the fruit for a numeric code") + def tool(params: FruitParams) -> str: + if calls is not None: + calls.append(params.code) + return fruit + + return tool + + +def lookup_vegetable(calls: list[int] | None = None) -> Tool: + @define_tool("lookup_vegetable", description="Looks up the vegetable for a numeric code") + def tool(params: VegetableParams) -> str: + if calls is not None: + calls.append(params.code) + return "carrot" + + return tool + + +def retired_lookup() -> Tool: + async def handler(_invocation: ToolInvocation) -> ToolResult: + return ToolResult(text_result_for_llm="retired") + + return Tool("retired_lookup", "Looks up a retired value", handler=handler) + + +def invalid_tool(calls: list[int]) -> Tool: + async def handler(_invocation: ToolInvocation) -> ToolResult: + calls.append(1) + return ToolResult(text_result_for_llm="never") + + return Tool("invalid.tool", "Has a name the runtime rejects", handler=handler) + + +def offered_tools(exchange: dict) -> set[str]: + """Names of the tools a model request offered.""" + return { + tool["function"]["name"] + for tool in exchange["request"].get("tools") or [] + if "function" in tool + } + + +def index_of_prompt(exchanges: list[dict], prompt: str) -> int: + """Index of the first model request that carries ``prompt`` as a user message.""" + for index, exchange in enumerate(exchanges): + for message in exchange["request"]["messages"]: + content = message.get("content") + text = content if isinstance(content, str) else json.dumps(content) + if message.get("role") == "user" and prompt in text: + return index + return -1 + + +class TestSetTools: + async def test_replaces_tools_on_a_created_session(self, ctx: E2ETestContext): + original_lookups: list[int] = [] + replacement_lookups: list[int] = [] + vegetable_lookups: list[int] = [] + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + tools=[lookup_fruit("apple", original_lookups), retired_lookup()], + ) + + first = await session.send_and_wait(FRUIT_PROMPT, timeout=30) + assert first is not None + assert "apple" in (first.data.content or "") + + await session.set_tools( + [ + lookup_fruit("dragonfruit", replacement_lookups), + lookup_vegetable(vegetable_lookups), + ] + ) + + second = await session.send_and_wait(FRUIT_AND_VEGETABLE_PROMPT, timeout=30) + assert second is not None + assert "dragonfruit" in (second.data.content or "") + assert "carrot" in (second.data.content or "") + assert original_lookups == [42] + assert replacement_lookups == [42] + assert vegetable_lookups == [7] + + # Model requests after the replacement offer exactly the new tool set. + exchanges = await ctx.get_exchanges() + replaced_from = index_of_prompt(exchanges, FRUIT_AND_VEGETABLE_PROMPT) + assert replaced_from > 0 + for exchange in exchanges[:replaced_from]: + tools = offered_tools(exchange) + assert {"lookup_fruit", "retired_lookup"} <= tools + assert "lookup_vegetable" not in tools + for exchange in exchanges[replaced_from:]: + tools = offered_tools(exchange) + assert {"lookup_fruit", "lookup_vegetable"} <= tools + assert "retired_lookup" not in tools + + await session.disconnect() + + async def test_replaces_tools_on_a_resumed_session(self, ctx: E2ETestContext): + created_lookups: list[int] = [] + created = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + tools=[lookup_fruit("apple", created_lookups)], + ) + session_id = created.session_id + first = await created.send_and_wait(FRUIT_PROMPT, timeout=30) + assert first is not None + assert "apple" in (first.data.content or "") + assert created_lookups == [42] + await created.disconnect() + + fruit_lookups: list[int] = [] + vegetable_lookups: list[int] = [] + resumed = await ctx.client.resume_session( + session_id, + on_permission_request=PermissionHandler.approve_all, + tools=[lookup_fruit("apple", fruit_lookups)], + ) + await resumed.set_tools([lookup_vegetable(vegetable_lookups)]) + + answer = await resumed.send_and_wait(VEGETABLE_PROMPT, timeout=30) + assert answer is not None + assert "carrot" in (answer.data.content or "") + assert vegetable_lookups == [7] + assert fruit_lookups == [] + + exchanges = await ctx.get_exchanges() + replaced_from = index_of_prompt(exchanges, VEGETABLE_PROMPT) + assert replaced_from > 0 + for exchange in exchanges[replaced_from:]: + tools = offered_tools(exchange) + assert "lookup_vegetable" in tools + assert "lookup_fruit" not in tools + + await resumed.disconnect() + + async def test_keeps_the_previous_tools_when_a_replacement_is_rejected( + self, ctx: E2ETestContext + ): + original_lookups: list[int] = [] + replacement_lookups: list[int] = [] + invalid_calls: list[int] = [] + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + tools=[lookup_fruit("apple", original_lookups)], + ) + + with pytest.raises(Exception): + await session.set_tools( + [ + lookup_fruit("dragonfruit", replacement_lookups), + invalid_tool(invalid_calls), + ] + ) + + answer = await session.send_and_wait(FRUIT_PROMPT, timeout=30) + assert answer is not None + assert "apple" in (answer.data.content or "") + assert original_lookups == [42] + assert replacement_lookups == [] + assert invalid_calls == [] + + await session.disconnect() diff --git a/python/e2e/test_subagent_hooks_e2e.py b/python/e2e/test_subagent_hooks_e2e.py index da70265a04..e06168f0ce 100644 --- a/python/e2e/test_subagent_hooks_e2e.py +++ b/python/e2e/test_subagent_hooks_e2e.py @@ -1,16 +1,28 @@ """ -Tests for sub-agent hooks functionality — verifies preToolUse/postToolUse hooks -fire for tool calls made by sub-agents spawned via the task tool. +Tests for sub-agent hooks — verifies preToolUse/postToolUse and +subagentStart/subagentStop for sub-agents spawned via the task tool. """ from __future__ import annotations +import asyncio +import json import os +import sys +from datetime import UTC, datetime +from uuid import uuid4 import httpx import pytest -from copilot import CopilotRequestContext, CopilotRequestHandler +from copilot import ( + CopilotRequestContext, + CopilotRequestHandler, + SubagentStartHookInput, + SubagentStartHookOutput, + SubagentStopHookInput, + SubagentStopHookOutput, +) from copilot.client import CopilotClient, RuntimeConnection from copilot.session import PermissionHandler @@ -19,10 +31,15 @@ pytestmark = pytest.mark.asyncio(loop_scope="module") +CHILD_CONTEXT = "Subagent start hook verified: read the requested file." +STOP_RESPONSE_PREFIX = "Subagent stop hook verified: " + class _RecordingRequestHandler(CopilotRequestHandler): def __init__(self) -> None: self.records: list[dict[str, str | None]] = [] + self.child_prompts: list[str] = [] + self.modified_parent_request = asyncio.Event() async def send_request( self, request: httpx.Request, ctx: CopilotRequestContext @@ -35,6 +52,26 @@ async def send_request( "interaction_type": ctx.interaction_type, } ) + if _is_inference_url(str(request.url)): + body = json.loads(request.content) + if ctx.parent_agent_id: + for message in body.get("messages", []): + if message["role"] != "user": + continue + content = message.get("content") + parts = [content] if isinstance(content, str) else content + if isinstance(parts, list): + for part in parts: + if isinstance(part, str): + text = part + elif isinstance(part, dict): + text = part.get("text") + else: + continue + if isinstance(text, str) and f"{CHILD_CONTEXT}\n\n" in text: + self.child_prompts.append(text) + elif STOP_RESPONSE_PREFIX in json.dumps(body): + self.modified_parent_request.set() return await super().send_request(request, ctx) @@ -64,12 +101,24 @@ def _assert_subagent_request_metadata(records: list[dict[str, str | None]]) -> N class TestSubagentHooks: - async def test_should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls( - self, ctx: E2ETestContext - ): - """Test that preToolUse/postToolUse hooks fire for sub-agent tool calls""" + async def test_should_apply_subagent_lifecycle_hook_outputs(self, ctx: E2ETestContext): + """Test tool and lifecycle hooks for a real subagent under the replay proxy.""" hook_log = [] + start_calls: list[tuple[SubagentStartHookInput, dict[str, str]]] = [] + stop_calls: list[tuple[SubagentStopHookInput, dict[str, str]]] = [] + subagent_stopped = asyncio.Event() request_handler = _RecordingRequestHandler() + waiting_text = ( + "I've launched an explore agent to read subagent-test.txt. " + "Waiting for it to complete..." + ) + parent_waiting = asyncio.Event() + parent_session_id = str(uuid4()) + final_text = ( + "The explore agent successfully read the file. " + "The contents of **subagent-test.txt** are:\n\n" + "```\nHello from subagent test!\n```" + ) async def on_pre_tool_use(input_data, invocation): hook_log.append( @@ -89,8 +138,27 @@ async def on_post_tool_use(input_data, invocation): "sessionId": input_data.get("sessionId"), } ) + # A fast child can inject its result before the fixture's waiting reply is requested. + if ( + input_data.get("toolName") == "view" + and input_data.get("sessionId") != parent_session_id + ): + await parent_waiting.wait() return None + async def on_subagent_start( + input_data: SubagentStartHookInput, invocation: dict[str, str] + ) -> SubagentStartHookOutput: + start_calls.append((input_data, invocation)) + return {"additionalContext": CHILD_CONTEXT} + + async def on_subagent_stop( + input_data: SubagentStopHookInput, invocation: dict[str, str] + ) -> SubagentStopHookOutput: + stop_calls.append((input_data, invocation)) + subagent_stopped.set() + return {"modifiedResponse": f"{STOP_RESPONSE_PREFIX}{input_data['response']}"} + # Create a client with the session-based subagents feature flag env = ctx.get_env() env["COPILOT_EXP_COPILOT_CLI_SESSION_BASED_SUBAGENTS"] = "true" @@ -105,38 +173,101 @@ async def on_post_tool_use(input_data, invocation): request_handler=request_handler, ) - session = await client.create_session( - on_permission_request=PermissionHandler.approve_all, - hooks={ - "on_pre_tool_use": on_pre_tool_use, - "on_post_tool_use": on_post_tool_use, - }, - ) + try: + session = await client.create_session( + session_id=parent_session_id, + on_permission_request=PermissionHandler.approve_all, + hooks={ + "on_pre_tool_use": on_pre_tool_use, + "on_post_tool_use": on_post_tool_use, + "on_subagent_start": on_subagent_start, + "on_subagent_stop": on_subagent_stop, + }, + ) + try: + write_file(ctx.work_dir, "subagent-test.txt", "Hello from subagent test!") - # Create a file for the sub-agent to read - write_file(ctx.work_dir, "subagent-test.txt", "Hello from subagent test!") + def on_event(event): + if ( + not event.agent_id + and event.type.value == "assistant.message" + and event.data.content == waiting_text + ): + parent_waiting.set() - await session.send_and_wait( - "Use the task tool to spawn an explore agent that reads the file " - "subagent-test.txt in the current directory and reports its contents. " - "You must use the task tool." - ) + unsubscribe = session.on(on_event) + try: + response = await session.send_and_wait( + "Use the task tool to spawn an explore agent that reads the file " + "subagent-test.txt in the current directory and reports its contents. " + "You must use the task tool." + ) + assert response is not None and not response.agent_id + assert response.data.content == final_text + replies = [ + event.data.content + for event in await session.get_events() + if not event.agent_id + and event.type.value == "assistant.message" + and event.data.content in (waiting_text, final_text) + ] + assert replies == [waiting_text, final_text] + await asyncio.wait_for(subagent_stopped.wait(), 120) + finally: + parent_waiting.set() + unsubscribe() - # Parent tool hooks fire for "task" - task_pre = [h for h in hook_log if h["kind"] == "pre" and h["toolName"] == "task"] - assert len(task_pre) >= 1, "preToolUse should fire for the parent's 'task' tool call" + assert len(start_calls) == 1, f"unexpected subagent starts: {start_calls}" + assert stop_calls, "subagentStop should fire after the explore agent finishes" + started, start_invocation = start_calls[0] + stopped, stop_invocation = stop_calls[-1] + assert start_invocation == stop_invocation == {"session_id": session.session_id} + assert started["sessionId"] == stopped["sessionId"] == session.session_id + assert stopped["timestamp"] >= started["timestamp"] + assert started["agentName"] == stopped["agentName"] == "explore" + assert stopped["agentType"] == "explore" + assert stopped["agentId"] + assert stopped["stopReason"] == "end_turn" + assert "Hello from subagent test!" in stopped["response"] + for input_data in (started, stopped): + assert isinstance(input_data["timestamp"], datetime) + assert input_data["timestamp"].tzinfo == UTC + assert os.path.normcase( + os.path.normpath(input_data["workingDirectory"]) + ) == os.path.normcase(os.path.normpath(ctx.work_dir)) + assert isinstance(input_data["transcriptPath"], str) + assert input_data.get("agentDisplayName") is None + assert input_data.get("agentDescription") is None - # Sub-agent tool hooks fire for "view" - view_pre = [h for h in hook_log if h["kind"] == "pre" and h["toolName"] == "view"] - view_post = [h for h in hook_log if h["kind"] == "post" and h["toolName"] == "view"] - assert len(view_pre) > 0, "preToolUse should fire for the sub-agent's 'view' tool call" - assert len(view_post) > 0, "postToolUse should fire for the sub-agent's 'view' tool call" - - # input.session_id distinguishes parent from sub-agent - assert view_pre[0]["sessionId"] != task_pre[0]["sessionId"], ( - "Sub-agent tool hooks should have a different sessionId than parent tool hooks" - ) - _assert_subagent_request_metadata(request_handler.records) + task_pre = [h for h in hook_log if h["kind"] == "pre" and h["toolName"] == "task"] + assert task_pre, "preToolUse should fire for the parent's 'task' tool call" - await session.disconnect() - await client.stop() + view_pre = [h for h in hook_log if h["kind"] == "pre" and h["toolName"] == "view"] + view_post = [h for h in hook_log if h["kind"] == "post" and h["toolName"] == "view"] + assert view_pre, "preToolUse should fire for the sub-agent's 'view' tool call" + assert view_post, "postToolUse should fire for the sub-agent's 'view' tool call" + assert view_pre[0]["sessionId"] != task_pre[0]["sessionId"], ( + "Sub-agent tool hooks should have a different sessionId than parent tool hooks" + ) + _assert_subagent_request_metadata(request_handler.records) + assert any( + f'{CHILD_CONTEXT}\n\nRead the file "subagent-test.txt"' in prompt + for prompt in request_handler.child_prompts + ), "start context must be prepended to the child's model prompt" + await asyncio.wait_for(request_handler.modified_parent_request.wait(), 120) + finally: + first_error = sys.exception() + try: + await session.disconnect() + except BaseException as cleanup_error: + if first_error is None: + raise + first_error.add_note(f"session.disconnect failed: {cleanup_error!r}") + finally: + first_error = sys.exception() + try: + await client.stop() + except BaseException as cleanup_error: + if first_error is None: + raise + first_error.add_note(f"client.stop failed: {cleanup_error!r}") diff --git a/python/e2e/test_tools_e2e.py b/python/e2e/test_tools_e2e.py index 303ddc3ac9..fe45ea3dba 100644 --- a/python/e2e/test_tools_e2e.py +++ b/python/e2e/test_tools_e2e.py @@ -1,9 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + """E2E Tools Tests""" import os import pytest -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, RootModel from copilot import ToolSet, define_tool from copilot.rpc import ( @@ -19,6 +21,31 @@ class TestTools: + async def test_string_schema_apply_patch_override_binds_patch_input(self, ctx: E2ETestContext): + patch = "*** Begin Patch\n*** Add File: override-marker.txt\n+from-native\n*** End Patch" + calls = [] + + @define_tool( + "apply_patch", + description="Apply a patch", + overrides_built_in_tool=True, + ) + def apply_patch(params: RootModel[str], invocation: ToolInvocation) -> str: + calls.append((params.root, invocation.arguments)) + return "HOST_PATCH_HANDLED" + + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, tools=[apply_patch] + ) + response = await session.send_and_wait( + "Use apply_patch to apply the supplied patch.", timeout=10.0 + ) + + assert calls == [(patch, patch)] + assert response is not None + assert response.data.content == "Host override completed." + assert not os.path.exists(os.path.join(ctx.work_dir, "override-marker.txt")) + async def test_invokes_built_in_tools(self, ctx: E2ETestContext): readme_path = os.path.join(ctx.work_dir, "README.md") with open(readme_path, "w") as f: diff --git a/python/test_cli_download.py b/python/test_cli_download.py index 5b144c2ce7..34e6f32a4d 100644 --- a/python/test_cli_download.py +++ b/python/test_cli_download.py @@ -29,6 +29,7 @@ def _release_package(runtime_platform: str) -> bytes: f"package/ripgrep/bin/{runtime_platform}/rg": b"ripgrep", "package/definitions/future.json": b"{}", "package/app.js": b"excluded", + "package/cli-main.js": b"excluded", "package/LICENSE.md": b"excluded", } buffer = io.BytesIO() @@ -87,6 +88,33 @@ def test_release_asset_uses_platform_package_name(monkeypatch): ) +@pytest.mark.parametrize( + ("version", "repository", "tag"), + [ + ("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"), + ], +) +def test_pinned_runtime_uses_matching_release_and_mirror(monkeypatch, version, repository, tag): + monkeypatch.delenv("COPILOT_CLI_DOWNLOAD_BASE_URL", raising=False) + asset = _cli_version.get_release_asset_name(version, "linux-x64") + expected = f"https://github.com/github/{repository}/releases/download/{tag}" + assert _cli_version.get_download_url(version, asset) == f"{expected}/{asset}" + assert _cli_version.get_checksums_url(version) == f"{expected}/SHA256SUMS.txt" + monkeypatch.setenv("COPILOT_CLI_DOWNLOAD_BASE_URL", "https://mirror.example/releases/") + assert ( + _cli_version.get_checksums_url(version) + == f"https://mirror.example/releases/{tag}/SHA256SUMS.txt" + ) + + @pytest.mark.parametrize( "member_name", ["package/../outside", r"package\..\outside"], @@ -133,8 +161,8 @@ def fetch(url: str, *, timeout: int) -> bytes: _cli_download.ensure_runtime_wrapper(version="1.2.3") -def test_cli_and_runtime_share_one_staged_bundle(tmp_path, monkeypatch): - version = "1.2.3" +@pytest.mark.parametrize("version", ["1.2.3", "1.2.3-4.unstable.r123.gabcdef0"]) +def test_cli_and_runtime_share_one_staged_bundle(tmp_path, monkeypatch, version): runtime_platform = "linux-x64" cli_name = "copilot.exe" if os.name == "nt" else "copilot" wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" @@ -163,6 +191,7 @@ def test_cli_and_runtime_share_one_staged_bundle(tmp_path, monkeypatch): assert (install_dir / "ripgrep" / "bin" / runtime_platform / "rg").read_bytes() == b"ripgrep" assert (install_dir / "definitions" / "future.json").read_bytes() == b"{}" assert not (install_dir / "app.js").exists() + assert not (install_dir / "cli-main.js").exists() assert (install_dir / ".hostless-runtime-assets-v2").is_file() assert fetch_mock.call_count == 2 if os.name != "nt": diff --git a/python/test_client.py b/python/test_client.py index 4dcb581463..57d284dd45 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -8,6 +8,8 @@ import inspect import json import os +import threading +from dataclasses import fields from datetime import UTC, datetime from tempfile import TemporaryDirectory from unittest.mock import AsyncMock, Mock, patch @@ -46,6 +48,9 @@ DiagnosticsConfiguration, DiagnosticSourcesConfiguration, MCPDiagnosticSourceConfiguration, + SessionFSReadFileBytesRequest, + SessionFSSetProviderCapabilities, + SessionFSWriteFileBytesRequest, ) from copilot.session import CopilotSession, PermissionHandler from copilot.session_events import ( @@ -56,10 +61,23 @@ SessionEvent, SessionEventType, ) +from copilot.session_fs_provider import SessionFsProvider, create_session_fs_adapter from copilot.tools import Tool from e2e.testharness import CLI_PATH +def test_session_fs_capabilities_preserve_positional_sqlite(): + capabilities = SessionFSSetProviderCapabilities(True) + + assert capabilities.sqlite is True + assert capabilities.binary is None + assert [field.name for field in fields(capabilities)] == ["sqlite", "binary"] + assert capabilities.to_dict() == {"sqlite": True} + assert SessionFSSetProviderCapabilities.from_dict( + {"sqlite": True, "binary": False} + ) == SessionFSSetProviderCapabilities(True, binary=False) + + def test_inprocess_connection_has_no_child_process_options(): connection = RuntimeConnection.for_inprocess() @@ -785,6 +803,167 @@ async def mock_request(method, params, **kwargs): finally: await client.force_stop() + @pytest.mark.asyncio + @pytest.mark.parametrize("cleanup_failure", [False, True]) + async def test_cloud_create_deletes_session_on_binary_provider_validation_failure( + self, cleanup_failure, caplog + ): + client = CopilotClient( + connection=RuntimeConnection.for_uri("localhost:1234"), + session_fs={ + "initial_working_directory": "/", + "session_state_path": "/session-state", + "conventions": "posix", + "capabilities": {"binary": True}, + }, + ) + requests = [] + + async def mock_request(method, params, **kwargs): + requests.append((method, params)) + if method == "session.create": + result = {"sessionId": "server-assigned-session"} + kwargs["on_response_inline"](result) + return result + if cleanup_failure: + return {"success": False, "error": "cleanup rejected"} + return {"success": True} + + client._client = Mock(request=mock_request) + with pytest.raises(ValueError, match="does not implement SessionFsBinaryProvider"): + await client.create_session( + cloud=CloudSessionOptions( + repository=CloudSessionRepository( + owner="github", name="copilot-sdk", branch="main" + ) + ), + create_session_fs_handler=lambda _: Mock(spec=SessionFsProvider), + ) + + assert "sessionId" not in requests[0][1] + assert requests[-1] == ("session.delete", {"sessionId": "server-assigned-session"}) + assert "server-assigned-session" not in client._sessions + if cleanup_failure: + assert "Failed to delete cloud session" in caplog.text + + @pytest.mark.asyncio + @pytest.mark.parametrize("valid_provider", [False, True]) + async def test_cloud_create_cancellation_waits_for_provider_validation(self, valid_provider): + client = CopilotClient( + connection=RuntimeConnection.for_uri("localhost:1234"), + session_fs={ + "initial_working_directory": "/", + "session_state_path": "/session-state", + "conventions": "posix", + "capabilities": {"binary": True}, + }, + ) + loop = asyncio.get_running_loop() + provider_started = asyncio.Event() + callback_finished = asyncio.Event() + delete_called = asyncio.Event() + release_provider = threading.Event() + requests = [] + + async def mock_request(method, params, **kwargs): + requests.append((method, params)) + if method == "session.create": + result = {"sessionId": "server-assigned-session"} + + def invoke_callback(): + try: + kwargs["on_response_inline"](result) + finally: + loop.call_soon_threadsafe(callback_finished.set) + + await asyncio.to_thread(invoke_callback) + return result + if method == "session.delete": + delete_called.set() + return {"success": True} + return {} + + def create_provider(_): + loop.call_soon_threadsafe(provider_started.set) + release_provider.wait() + provider = Mock(spec=SessionFsProvider) + if valid_provider: + provider.read_file_bytes = AsyncMock(return_value=b"image") + provider.write_file_bytes = AsyncMock() + return provider + + client._client = Mock(request=mock_request) + creation = asyncio.create_task( + client.create_session( + cloud=CloudSessionOptions( + repository=CloudSessionRepository( + owner="github", name="copilot-sdk", branch="main" + ) + ), + create_session_fs_handler=create_provider, + ) + ) + try: + await asyncio.wait_for(provider_started.wait(), timeout=5) + creation.cancel() + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(creation, timeout=5) + assert not delete_called.is_set() + + release_provider.set() + await asyncio.wait_for(callback_finished.wait(), timeout=5) + if valid_provider: + assert not delete_called.is_set() + else: + await asyncio.wait_for(delete_called.wait(), timeout=5) + assert requests[-1] == ( + "session.delete", + {"sessionId": "server-assigned-session"}, + ) + assert "server-assigned-session" not in client._sessions + finally: + release_provider.set() + if not creation.done(): + creation.cancel() + await asyncio.gather(creation, return_exceptions=True) + + @pytest.mark.asyncio + async def test_cloud_create_preserves_session_when_interest_registration_fails(self): + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + requests = [] + + async def mock_request(method, params, **kwargs): + requests.append((method, params)) + if method == "session.create": + result = {"sessionId": "server-assigned-session"} + kwargs["on_response_inline"](result) + return result + if method == "session.eventLog.registerInterest": + raise RuntimeError("interest registration failed") + return {} + + client._client = Mock(request=mock_request) + with pytest.raises(RuntimeError, match="interest registration failed"): + await client.create_session( + cloud=CloudSessionOptions( + repository=CloudSessionRepository( + owner="github", name="copilot-sdk", branch="main" + ) + ), + on_mcp_auth_request=lambda request: {"kind": "cancelled"}, + ) + + assert "sessionId" not in requests[0][1] + assert ( + "session.eventLog.registerInterest", + { + "sessionId": "server-assigned-session", + "eventType": "mcp.oauth_required", + }, + ) in requests + assert not any(method == "session.delete" for method, _ in requests) + assert "server-assigned-session" not in client._sessions + @pytest.mark.asyncio async def test_create_and_resume_session_forward_github_mcp_tool_config(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) @@ -1694,6 +1873,46 @@ async def test_connect_via_tcp_uses_family_independent_resolution(self): class TestSessionFsConfig: + @pytest.mark.asyncio + async def test_binary_write_preserves_bytes_and_rejects_invalid_base64(self): + provider = Mock(spec=SessionFsProvider) + provider.read_file_bytes = AsyncMock(return_value=b"\x00\xff") + provider.write_file_bytes = AsyncMock() + adapter = create_session_fs_adapter(provider) + request = SessionFSWriteFileBytesRequest( + session_id="session", path="/image.png", content="AP/+AQ==", mode=0o600 + ) + assert await adapter.write_file_bytes(request) is None + provider.write_file_bytes.assert_awaited_once_with("/image.png", b"\x00\xff\xfe\x01", 0o600) + for content in ("AA==AAAA", "AA==\n", "A" * (64 * 1024 * 1024)): + error = await adapter.write_file_bytes( + SessionFSWriteFileBytesRequest( + session_id="session", path="/image.png", content=content + ) + ) + assert error is not None + assert error.code.value == "UNKNOWN" + provider.write_file_bytes.assert_awaited_once() + + @pytest.mark.asyncio + async def test_binary_read_rejects_oversized_provider_bytes(self): + provider = Mock(spec=SessionFsProvider) + provider.read_file_bytes = AsyncMock( + return_value=bytes((64 * 1024 * 1024 - 1024) // 4 * 3 + 1) + ) + provider.write_file_bytes = AsyncMock() + result = await create_session_fs_adapter(provider).read_file_bytes( + SessionFSReadFileBytesRequest(session_id="session", path="/image.png") + ) + + provider.read_file_bytes.assert_awaited_once_with("/image.png") + assert result.content == "" + assert result.error is not None + assert result.error.code.value == "UNKNOWN" + assert ( + result.error.message == "sessionFs.readFileBytes content exceeds the binary read limit" + ) + def test_missing_initial_cwd(self): with pytest.raises(ValueError, match="session_fs.initial_working_directory is required"): CopilotClient( @@ -2372,6 +2591,7 @@ async def mock_request(method, params, **kwargs): "headers": {"Authorization": "Bearer provider-token"}, "model_id": "gpt-4o", "wire_model": "my-finetune-v3", + "model_provider": "lm_studio", "max_prompt_tokens": 100_000, "max_output_tokens": 4096, "transport": "websockets", @@ -2383,6 +2603,7 @@ async def mock_request(method, params, **kwargs): assert provider["headers"] == {"Authorization": "Bearer provider-token"} assert provider["modelId"] == "gpt-4o" assert provider["wireModel"] == "my-finetune-v3" + assert provider["modelProvider"] == "lm_studio" assert provider["maxPromptTokens"] == 100_000 assert provider["maxOutputTokens"] == 4096 assert provider["transport"] == "websockets" @@ -2417,6 +2638,7 @@ async def mock_request(method, params, **kwargs): "headers": {"Authorization": "Bearer resume-token"}, "model_id": "gpt-4o", "wire_model": "my-finetune-v3", + "model_provider": "ollama", "max_prompt_tokens": 100_000, "max_output_tokens": 4096, }, @@ -2427,11 +2649,53 @@ async def mock_request(method, params, **kwargs): assert provider["headers"] == {"Authorization": "Bearer resume-token"} assert provider["modelId"] == "gpt-4o" assert provider["wireModel"] == "my-finetune-v3" + assert provider["modelProvider"] == "ollama" assert provider["maxPromptTokens"] == 100_000 assert provider["maxOutputTokens"] == 4096 finally: await client.force_stop() + @pytest.mark.asyncio + async def test_create_session_forwards_named_provider_model_provider(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + + try: + captured = {} + original_request = client._client.request + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method == "session.create": + sid = params.get("sessionId") or "session-id" + result = {"sessionId": sid} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + return await original_request(method, params, **kwargs) + + client._client.request = mock_request + await client.create_session( + on_permission_request=PermissionHandler.approve_all, + providers=[ + { + "name": "local", + "type": "openai", + "base_url": "http://localhost:11434/v1", + "model_provider": "ollama", + } + ], + models=[{"id": "llama3", "provider": "local"}], + ) + + provider = captured["session.create"]["providers"][0] + assert provider["name"] == "local" + assert provider["baseUrl"] == "http://localhost:11434/v1" + assert provider["modelProvider"] == "ollama" + finally: + await client.force_stop() + @pytest.mark.asyncio async def test_session_send_forwards_request_headers(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) @@ -2621,6 +2885,71 @@ async def mock_request(method, params, **kwargs): finally: await client.force_stop() + @pytest.mark.asyncio + @pytest.mark.parametrize( + ("mode", "setting", "expected"), + [ + ("copilot-cli", None, None), + ("empty", None, None), + ("copilot-cli", False, False), + ("copilot-cli", True, True), + ("empty", False, False), + ("empty", True, True), + ], + ) + async def test_resume_transcript_recovery_wire_and_report( + self, mode, setting, expected, tmp_path + ): + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + mode=mode, + base_directory=str(tmp_path), + ) + await client.start() + try: + captured = {} + + async def mock_request(method, params, **_kwargs): + captured[method] = params + if method == "session.resume": + if params["sessionId"] == "transcript-unrepaired": + return {"sessionId": params["sessionId"]} + return { + "sessionId": params["sessionId"], + "transcriptRecovery": { + "plannedBackupPath": "backup.jsonl", + "invalidLineNumbers": [3, 4], + "sessionStartMoved": True, + }, + } + if method == "session.options.update": + return {"success": True} + return {} + + client._client.request = mock_request + session = await client.resume_session( + "transcript-test", + on_permission_request=PermissionHandler.approve_all, + available_tools=[], + allow_transcript_recovery=setting, + ) + assert captured["session.resume"].get("allowTranscriptRecovery") is expected + assert ("allowTranscriptRecovery" in captured["session.resume"]) == ( + expected is not None + ) + assert session.transcript_recovery is not None + assert session.transcript_recovery.planned_backup_path == "backup.jsonl" + assert session.transcript_recovery.invalid_line_numbers == [3, 4] + assert session.transcript_recovery.session_start_moved is True + unrepaired = await client.resume_session( + "transcript-unrepaired", + on_permission_request=PermissionHandler.approve_all, + available_tools=[], + ) + assert unrepaired.transcript_recovery is None + finally: + await client.force_stop() + @pytest.mark.asyncio async def test_resume_session_forwards_continue_pending_work(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) diff --git a/python/test_connectors.py b/python/test_connectors.py index 9c7e63a435..eb73ac597f 100644 --- a/python/test_connectors.py +++ b/python/test_connectors.py @@ -1,8 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + from unittest.mock import AsyncMock, Mock import pytest from copilot.generated.rpc import ( + AuthInfoType, ConnectorAccountRequest, ConnectorAvailability, ConnectorConnectRequest, @@ -21,6 +24,8 @@ async def test_session_connectors_exposes_host_lifecycle_api(): capabilities = await session.rpc.connectors.get_capabilities() assert capabilities.availability is ConnectorAvailability.ENABLED + assert capabilities.session_account_selection is None + assert capabilities.targeted_reconcile is None assert (await session.rpc.connectors.get_status()).api_version == 1 assert ( await session.rpc.connectors.list(ConnectorAccountRequest(account_id="account-1")) @@ -113,6 +118,150 @@ async def test_session_connectors_exposes_host_lifecycle_api(): ] +@pytest.mark.asyncio +async def test_unknown_connector_continuation_outcome_is_not_connected(): + transport = Mock() + transport.request = AsyncMock( + return_value={ + "kind": "future_outcome", + "continuationId": "continuation-1", + "status": connector_status(), + } + ) + session = CopilotSession("session-1", transport) + + with pytest.raises(ValueError, match="future_outcome"): + await session.rpc.connectors.continue_connection( + ConnectorContinueRequest( + continuation_id="continuation-1", + max_attempts=1, + poll_interval_ms=0, + deadline_ms=1_000, + ) + ) + + +@pytest.mark.asyncio +async def test_session_connector_account_and_targeted_reconcile_use_exact_wire_contract(): + account = { + "accountId": "session-account-1", + "authInfo": {"type": "token", "host": "github.com", "login": "alice"}, + } + transport = Mock() + transport.request = AsyncMock( + side_effect=[ + { + **connector_response("session.connectors.getCapabilities", {}), + "sessionAccountSelection": True, + "targetedReconcile": True, + }, + account, + None, + connector_status(), + ] + ) + session = CopilotSession("session-1", transport) + + capabilities = await session.rpc.connectors.get_capabilities() + assert capabilities.availability is ConnectorAvailability.ENABLED + assert capabilities.session_account_selection is True + assert capabilities.targeted_reconcile is True + selected = await session.rpc.connectors.get_account() + assert selected is not None + assert selected.account_id == "session-account-1" + assert selected.auth_info.type is AuthInfoType.TOKEN + assert selected.to_dict() == account + assert await session.rpc.connectors.get_account() is None + await session.rpc.connectors.reconcile( + ConnectorReconcileRequest( + account_id=selected.account_id, + refresh_catalog=True, + force_connector_name="calendar", + ) + ) + + assert [(call.args[0], call.args[1]) for call in transport.request.await_args_list] == [ + ("session.connectors.getCapabilities", {"sessionId": "session-1"}), + ("session.connectors.getAccount", {"sessionId": "session-1"}), + ("session.connectors.getAccount", {"sessionId": "session-1"}), + ( + "session.connectors.reconcile", + { + "sessionId": "session-1", + "accountId": "session-account-1", + "refreshCatalog": True, + "forceConnectorName": "calendar", + }, + ), + ] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("supported", [None, False]) +async def test_hosts_can_avoid_unsupported_session_connector_calls(supported): + capabilities = connector_response("session.connectors.getCapabilities", {}) + if supported is not None: + capabilities.update(sessionAccountSelection=supported, targetedReconcile=supported) + transport = Mock() + transport.request = AsyncMock(return_value=capabilities) + session = CopilotSession("session-1", transport) + + result = await session.rpc.connectors.get_capabilities() + if result.availability is ConnectorAvailability.ENABLED: + if result.session_account_selection is True: + await session.rpc.connectors.get_account() + if result.targeted_reconcile is True: + await session.rpc.connectors.reconcile( + ConnectorReconcileRequest(account_id="account-1", force_connector_name="calendar") + ) + + transport.request.assert_awaited_once_with( + "session.connectors.getCapabilities", {"sessionId": "session-1"} + ) + + +@pytest.mark.asyncio +async def test_connector_catalog_presentation_metadata_is_optional(): + entry = { + "name": "calendar", + "displayName": "Calendar", + "status": "connected", + "runtimeServerIds": ["connector-calendar"], + } + decorated = { + **entry, + "logo": "https://example.com/calendar.svg", + "tier": "standard", + "releaseTag": "preview", + } + transport = Mock() + transport.request = AsyncMock( + side_effect=[ + {"connectors": [decorated], "revision": 1, "refreshedAtMs": 1}, + {"connectors": [entry], "revision": 2, "refreshedAtMs": 2}, + ] + ) + session = CopilotSession("session-1", transport) + catalog = await session.rpc.connectors.list(ConnectorAccountRequest(account_id="account-1")) + assert catalog.connectors[0].to_dict() == decorated + legacy = await session.rpc.connectors.refresh(ConnectorAccountRequest(account_id="account-1")) + assert legacy.connectors[0].logo is None + assert legacy.connectors[0].tier is None + assert legacy.connectors[0].release_tag is None + assert legacy.connectors[0].to_dict() == entry + + +def test_connector_reconcile_request_keeps_published_positional_order(): + request = ConnectorReconcileRequest("account-1", True) + assert request.refresh_catalog is True + assert request.force_connector_name is None + assert request.to_dict() == {"accountId": "account-1", "refreshCatalog": True} + targeted = ConnectorReconcileRequest("account-1", True, force_connector_name="mail") + assert targeted.force_connector_name == "mail" + with pytest.raises(TypeError): + ConnectorReconcileRequest("account-1", True, "mail") + + def connector_response(method, _params, **_kwargs): if method == "session.connectors.getCapabilities": return { diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py index eeb1170f53..401eec6359 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -30,6 +30,7 @@ SessionResumeData, SessionStartData, SessionTaskCompleteData, + ToolExecutionCompleteData, UserMessageAgentMode, session_event_from_dict, session_event_to_dict, @@ -39,6 +40,30 @@ class TestEventForwardCompatibility: """Test forward compatibility for unknown event types.""" + def test_future_file_edit_kind_preserves_tool_completion(self): + wire = { + "id": str(uuid4()), + "timestamp": datetime.now().isoformat(), + "parentId": None, + "type": "tool.execution_complete", + "data": { + "toolCallId": "call-1", + "success": True, + "fileEdits": [{"path": "/future.txt", "kind": "append"}], + }, + } + + event = session_event_from_dict(wire) + assert isinstance(event.data, ToolExecutionCompleteData) + assert event.data.file_edits is not None + assert event.data.file_edits[0].kind.name == "UNKNOWN" + assert event.data.file_edits[0].kind.value == "append" + assert session_event_to_dict(event)["data"]["fileEdits"] == wire["data"]["fileEdits"] + other = session_event_from_dict(wire) + assert event.data.file_edits[0].kind is other.data.file_edits[0].kind + assert event.data.file_edits[0] == other.data.file_edits[0] + assert len({event.data.file_edits[0].kind, other.data.file_edits[0].kind}) == 1 + @pytest.mark.parametrize( ("event_type", "data"), [("session.idle", {}), ("user.message", {"content": "hello"})] ) diff --git a/python/test_ffi_runtime_host.py b/python/test_ffi_runtime_host.py index 5806afa1e1..b142985825 100644 --- a/python/test_ffi_runtime_host.py +++ b/python/test_ffi_runtime_host.py @@ -1,7 +1,11 @@ +import asyncio import threading import time from unittest.mock import patch +import pytest + +from copilot import CopilotClient, RuntimeConnection from copilot._ffi_runtime_host import FfiRuntimeHost @@ -54,3 +58,43 @@ def test_dispose_retains_callback_until_connection_close_succeeds(): time.sleep(0.05) assert library.close_calls == close_calls_after_cleanup assert library.shutdown_calls == 1 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("method", ["stop", "force_stop"]) +async def test_client_shutdown_keeps_event_loop_responsive_until_native_cleanup_finishes(method): + loop = asyncio.get_running_loop() + loop_response = threading.Event() + loop_responded_during_shutdown = False + + class BlockingLibrary(_TestLibrary): + def host_shutdown(self, server_id: int) -> bool: + nonlocal loop_responded_during_shutdown + loop.call_soon_threadsafe(loop_response.set) + loop_responded_during_shutdown = loop_response.wait(5) + return super().host_shutdown(server_id) + + library = BlockingLibrary() + library.allow_close = True + with patch("copilot._ffi_runtime_host._load_library", return_value=library): + host = FfiRuntimeHost("test-runtime", None) + host._server_id = 11 + host._connection_id = 21 + host._outbound_callback = object() + + client = CopilotClient(connection=RuntimeConnection.for_stdio(path="copilot")) + client._ffi_host = host + client._process = host.process + client._state = "connected" + + await getattr(client, method)() + + assert loop_responded_during_shutdown, "Native shutdown blocked the asyncio event loop" + assert library.shutdown.is_set(), "Client shutdown returned before native cleanup" + assert library.shutdown_calls == 1 + assert host._server_id == 0 + assert host._connection_id == 0 + assert host._outbound_callback is None + assert client._ffi_host is None + assert client._process is None + assert client._state == "disconnected" diff --git a/python/test_host.py b/python/test_host.py index c002cd0d5a..546d797ccb 100644 --- a/python/test_host.py +++ b/python/test_host.py @@ -8,6 +8,7 @@ from copilot import AhpHostOptions, CopilotClient, RuntimeConnection from copilot._jsonrpc import JsonRpcClient from copilot.generated.rpc import ServerRpc +from copilot.rpc import HostGitHubEnvironmentOptions, HostLocalServerOptions def client_fixture(): @@ -47,15 +48,62 @@ def release(client, host): ) +@pytest.mark.parametrize("options", [None, AhpHostOptions()]) +async def test_requires_explicit_transport(options): + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + client.start = AsyncMock() + with pytest.raises(ValueError, match="At least one"): + await client.start_ahp_host(options) + client.start.assert_not_called() + + +@pytest.mark.parametrize("local", [False, True]) +async def test_github_hosting_with_optional_local_transport(local): + client, rpc = client_fixture() + + async def start(method, params): + assert method == "host.start" + return { + "hostId": params["hostId"], + "environmentId": "environment-1", + **({"url": "ws://127.0.0.1:12345", "token": "local-token"} if local else {}), + } + + rpc.request.side_effect = start + host = await client.start_ahp_host( + AhpHostOptions( + github_environment=HostGitHubEnvironmentOptions( + name="Application", compute_id="stable-installation-id" + ), + local_server=HostLocalServerOptions() if local else None, + ) + ) + assert rpc.request.call_args.args[1] == { + "hostId": host.host_id, + "githubEnvironment": {"name": "Application", "computeId": "stable-installation-id"}, + **({"localServer": {}} if local else {}), + } + assert host.environment_id == "environment-1" + assert host.url == ("ws://127.0.0.1:12345" if local else None) + assert host.token == ("local-token" if local else None) + assert host.pid is None + + async def test_handle_uses_original_transport_and_forwards_every_disposal(): client, rpc = client_fixture() exited = Mock() host = await client.start_ahp_host( - AhpHostOptions(port=0, require_connection_token=False, on_exit=exited) + AhpHostOptions( + local_server=HostLocalServerOptions(port=0, require_connection_token=False), + on_exit=exited, + ) ) assert host.pid is None params = rpc.request.call_args_list[0].args[1] - assert params == {"hostId": host.host_id, "port": 0, "requireConnectionToken": False} + assert params == { + "hostId": host.host_id, + "localServer": {"port": 0, "requireConnectionToken": False}, + } replacement = Mock(request=AsyncMock()) client._client = replacement published = await host.publish_session("resident") @@ -94,7 +142,12 @@ async def restore(request): return original host = await client.start_ahp_host( - AhpHostOptions(create_session=create, resume_session=restore, on_session_released=released) + AhpHostOptions( + local_server=HostLocalServerOptions(), + create_session=create, + resume_session=restore, + on_session_released=released, + ) ) config = ( {"continuePendingWork": False, "suppressResumeEvent": True} @@ -135,7 +188,9 @@ async def test_resume_can_return_a_retained_original_without_reconfiguration(): client, rpc = client_fixture() original = await client.create_session(session_id="session", working_directory="/workspace") factory = AsyncMock(return_value=original) - host = await client.start_ahp_host(AhpHostOptions(resume_session=factory)) + host = await client.start_ahp_host( + AhpHostOptions(local_server=HostLocalServerOptions(), resume_session=factory) + ) rpc.request.reset_mock() assert await client._ahp_hosts.materialize(handoff(host, resume=True)) == { "sessionId": "session" @@ -161,7 +216,11 @@ async def create(request): return original host = await client.start_ahp_host( - AhpHostOptions(create_session=create, on_session_released=released) + AhpHostOptions( + local_server=HostLocalServerOptions(), + create_session=create, + on_session_released=released, + ) ) with pytest.raises(ValueError, match="AHP callback must"): await client._ahp_hosts.materialize(handoff(host)) @@ -189,7 +248,11 @@ def on_released(session): delivered.set() host = await client.start_ahp_host( - AhpHostOptions(create_session=create, on_session_released=on_released) + AhpHostOptions( + local_server=HostLocalServerOptions(), + create_session=create, + on_session_released=on_released, + ) ) task = asyncio.create_task(client._ahp_hosts.materialize(handoff(host))) await asyncio.wait_for(started.wait(), 1) @@ -219,7 +282,11 @@ async def factory(request): await work host = await client.start_ahp_host( - AhpHostOptions(create_session=factory, resume_session=factory) + AhpHostOptions( + local_server=HostLocalServerOptions(), + create_session=factory, + resume_session=factory, + ) ) rpc = JsonRpcClient(Mock()) rpc._send_message = AsyncMock() @@ -250,7 +317,11 @@ async def test_start_failure_removes_factory_and_exit_callback(): exited = Mock() rpc.request.side_effect = RuntimeError("bind failed") with pytest.raises(RuntimeError, match="bind failed"): - await client.start_ahp_host(AhpHostOptions(create_session=AsyncMock(), on_exit=exited)) + await client.start_ahp_host( + AhpHostOptions( + local_server=HostLocalServerOptions(), create_session=AsyncMock(), on_exit=exited + ) + ) client._ahp_hosts.disconnect() await asyncio.sleep(0) exited.assert_not_called() @@ -281,7 +352,11 @@ async def request(method, params, **_): return {} rpc.request.side_effect = request - task = asyncio.create_task(client.start_ahp_host(AhpHostOptions(create_session=AsyncMock()))) + task = asyncio.create_task( + client.start_ahp_host( + AhpHostOptions(local_server=HostLocalServerOptions(), create_session=AsyncMock()) + ) + ) await asyncio.wait_for(accepted.wait(), 1) if cancel_mode == "cancel": task.cancel() @@ -320,7 +395,9 @@ async def restore(request): raise RuntimeError("application setup failed") host = await client.start_ahp_host( - AhpHostOptions(create_session=create, resume_session=restore) + AhpHostOptions( + local_server=HostLocalServerOptions(), create_session=create, resume_session=restore + ) ) with pytest.raises(RuntimeError, match="application setup failed"): await client._ahp_hosts.materialize(handoff(host, resume=resume)) @@ -335,7 +412,10 @@ async def restore(request): async def test_factory_error_propagates_and_release_callback_errors_are_logged(caplog): client, _ = client_fixture() host = await client.start_ahp_host( - AhpHostOptions(create_session=AsyncMock(side_effect=ValueError("factory failed"))) + AhpHostOptions( + local_server=HostLocalServerOptions(), + create_session=AsyncMock(side_effect=ValueError("factory failed")), + ) ) with pytest.raises(ValueError, match="factory failed"): await client._ahp_hosts.materialize(handoff(host)) @@ -347,7 +427,11 @@ async def create(request): return await client.create_session(**request.config) host = await client.start_ahp_host( - AhpHostOptions(create_session=create, on_session_released=fail_release) + AhpHostOptions( + local_server=HostLocalServerOptions(), + create_session=create, + on_session_released=fail_release, + ) ) await client._ahp_hosts.materialize(handoff(host)) release(client, host) diff --git a/python/test_mcp_prompts_generated.py b/python/test_mcp_prompts_generated.py new file mode 100644 index 0000000000..2c62c4c231 --- /dev/null +++ b/python/test_mcp_prompts_generated.py @@ -0,0 +1,72 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + +"""Generated prompt RPC contracts; real MCP behavior is covered by Node and .NET E2Es.""" + +import json +from pathlib import Path +from unittest.mock import AsyncMock + +import pytest + +from copilot.rpc import MCPPromptsGetRequest, MCPPromptsListRequest, SessionRpc + + +@pytest.fixture +def prompt_fixtures(): + path = Path(__file__).parent.parent / "test" / "harness" / "mcp-prompt-fixtures.json" + return json.loads(path.read_text(encoding="utf-8")) + + +@pytest.mark.asyncio +async def test_prompt_list_dispatch_and_optional_fields(prompt_fixtures): + client = AsyncMock() + api = SessionRpc(client, "bound-session") + client.request.return_value = prompt_fixtures["firstPage"] + + first = await api.mcp.prompts.list(MCPPromptsListRequest(server_name="fixture")) + + client.request.assert_awaited_once_with( + "session.mcp.prompts.list", {"sessionId": "bound-session", "serverName": "fixture"} + ) + assert first.to_dict() == prompt_fixtures["firstPage"] + assert [argument.required for argument in first.prompts[0].arguments] == [True, False, None] + + client.request.reset_mock() + client.request.return_value = prompt_fixtures["secondPage"] + second = await api.mcp.prompts.list( + MCPPromptsListRequest(server_name="fixture", cursor=first.next_cursor) + ) + client.request.assert_awaited_once_with( + "session.mcp.prompts.list", + {"sessionId": "bound-session", "serverName": "fixture", "cursor": first.next_cursor}, + ) + assert second.to_dict() == prompt_fixtures["secondPage"] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("arguments", [None, {}, {"topic": "日本語", "style": ""}]) +async def test_prompt_get_dispatch_and_opaque_json(prompt_fixtures, arguments): + client = AsyncMock() + client.request.return_value = prompt_fixtures["richPrompt"] + api = SessionRpc(client, "bound-session") + + result = await api.mcp.prompts.get( + MCPPromptsGetRequest(server_name="fixture", prompt_name="rich", arguments=arguments) + ) + + expected = {"sessionId": "bound-session", "serverName": "fixture", "promptName": "rich"} + if arguments is not None: + expected["arguments"] = arguments + client.request.assert_awaited_once_with("session.mcp.prompts.get", expected) + assert result.to_dict() == prompt_fixtures["richPrompt"] + + +@pytest.mark.asyncio +async def test_prompt_get_propagates_rpc_error(): + client = AsyncMock() + client.request.side_effect = RuntimeError("Missing required argument: topic") + + with pytest.raises(RuntimeError, match="Missing required argument: topic"): + await SessionRpc(client, "bound-session").mcp.prompts.get( + MCPPromptsGetRequest(server_name="fixture", prompt_name="rich") + ) diff --git a/python/test_rpc_event_log_fixture.py b/python/test_rpc_event_log_fixture.py deleted file mode 100644 index fd3c97e0bb..0000000000 --- a/python/test_rpc_event_log_fixture.py +++ /dev/null @@ -1,184 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. - -"""Deterministic controls for the event-log E2E scenarios' background-event races.""" - -import asyncio -from types import SimpleNamespace -from unittest.mock import AsyncMock - -import pytest - -from copilot.rpc import EventsCursorStatus, EventsReadResult, SessionsSaveRequest -from copilot.session_events import ( - PlanChangedOperation, - SessionEventType, - SessionPlanChangedData, - SessionTitleChangedData, -) -from e2e import test_rpc_event_log_e2e as scenario - - -@pytest.mark.asyncio -@pytest.mark.parametrize("ephemeral", [False, True]) -async def test_tail_scenario_ignores_only_background_ephemeral_events(ephemeral): - background = SimpleNamespace( - type=SessionEventType.SESSION_INDEXED_SEARCH, - ephemeral=ephemeral, - ) - update_plan = AsyncMock() - save_session = AsyncMock() - - async def read(request): - assert request.cursor == "tail-before-indexing" - if save_session.await_count: - update_plan.assert_awaited_once() - events = [ - SimpleNamespace( - type=SessionEventType.SESSION_PLAN_CHANGED, - data=SessionPlanChangedData(operation=PlanChangedOperation.CREATE), - ephemeral=False, - ) - ] - else: - events = [] if ephemeral and request.include_ephemeral is False else [background] - return EventsReadResult( - cursor="tail-after-indexing", - cursor_status=EventsCursorStatus.OK, - events=events, - has_more=False, - ) - - session = SimpleNamespace( - session_id="tail-session", - log=AsyncMock(), - rpc=SimpleNamespace( - plan=SimpleNamespace(update=update_plan), - event_log=SimpleNamespace( - tail=AsyncMock(return_value=SimpleNamespace(cursor="tail-before-indexing")), - read=read, - ), - ), - disconnect=AsyncMock(), - ) - ctx = SimpleNamespace( - client=SimpleNamespace( - create_session=AsyncMock(return_value=session), - rpc=SimpleNamespace(sessions=SimpleNamespace(save=save_session)), - ) - ) - - run = ( - scenario.TestRpcEventLog().test_should_return_tail_cursor_and_read_empty_when_no_new_events - ) - if ephemeral: - await run(ctx) - update_plan.assert_awaited_once() - save_session.assert_awaited_once_with(SessionsSaveRequest(session_id="tail-session")) - session.log.assert_awaited_once_with("Ephemeral event after tail", ephemeral=True) - else: - with pytest.raises(AssertionError): - await run(ctx) - - session.disconnect.assert_awaited_once() - - -@pytest.mark.asyncio -@pytest.mark.parametrize("background_wakeups", [0, 2]) -async def test_filtered_long_poll_scenario_continues_after_unmatched_events(background_wakeups): - expected_title = None - cursors = ["initial-tail", *[f"background-{i}" for i in range(background_wakeups)]] - reads = [] - - async def set_name(request): - nonlocal expected_title - expected_title = request.name - - async def read(request): - index = len(reads) - assert request.cursor == cursors[index] - assert request.types == ["session.title_changed"] - assert request.wait_ms == 5000 - reads.append(request) - if index < background_wakeups: - return EventsReadResult( - cursor=cursors[index + 1], - cursor_status=EventsCursorStatus.OK, - events=[], - has_more=False, - ) - assert expected_title is not None - return EventsReadResult( - cursor="after-title", - cursor_status=EventsCursorStatus.OK, - events=[ - SimpleNamespace( - type=SessionEventType.SESSION_TITLE_CHANGED, - data=SessionTitleChangedData(title=expected_title), - ) - ], - has_more=False, - ) - - session = SimpleNamespace( - rpc=SimpleNamespace( - name=SimpleNamespace(set=set_name), - event_log=SimpleNamespace( - tail=AsyncMock(return_value=SimpleNamespace(cursor=cursors[0])), - read=read, - ), - ), - disconnect=AsyncMock(), - ) - ctx = SimpleNamespace(client=SimpleNamespace(create_session=AsyncMock(return_value=session))) - - await ( - scenario.TestRpcEventLog().test_should_long_poll_with_types_filter_for_title_changed_event( - ctx - ) - ) - - assert len(reads) == background_wakeups + 1 - session.disconnect.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_filtered_long_poll_scenario_cancels_reader_on_name_set_failure(): - read_started = asyncio.Event() - read_cancelled = asyncio.Event() - reader = None - - async def read(_request): - nonlocal reader - reader = asyncio.current_task() - read_started.set() - try: - await asyncio.Future() - finally: - read_cancelled.set() - - async def set_name(_request): - await read_started.wait() - raise RuntimeError("controlled name.set failure") - - session = SimpleNamespace( - rpc=SimpleNamespace( - name=SimpleNamespace(set=set_name), - event_log=SimpleNamespace( - tail=AsyncMock(return_value=SimpleNamespace(cursor="tail")), - read=read, - ), - ), - disconnect=AsyncMock(), - ) - ctx = SimpleNamespace(client=SimpleNamespace(create_session=AsyncMock(return_value=session))) - - suite = scenario.TestRpcEventLog() - try: - with pytest.raises(RuntimeError, match="controlled name.set failure"): - await suite.test_should_long_poll_with_types_filter_for_title_changed_event(ctx) - assert read_cancelled.is_set() - session.disconnect.assert_awaited_once() - finally: - if reader is not None: - reader.cancel() - await asyncio.gather(reader, return_exceptions=True) diff --git a/python/test_rpc_generated.py b/python/test_rpc_generated.py index cf736be5fa..df5dc87232 100644 --- a/python/test_rpc_generated.py +++ b/python/test_rpc_generated.py @@ -7,7 +7,13 @@ import pytest from copilot._jsonrpc import JsonRpcClient -from copilot.generated.rpc import ClientGlobalApiHandlers, register_client_global_api_handlers +from copilot.generated.rpc import ( + ClientGlobalApiHandlers, + CustomizationReloadOutcome, + CustomizationReloadStatus, + CustomizationReloadSubsystem, + register_client_global_api_handlers, +) from copilot.rpc import ( BuiltinToolInputSchemaType, CommandsApi, @@ -31,6 +37,25 @@ ) +def test_customization_reload_outcome_preserves_future_enum_values(): + payload = {"status": "newStatus", "subsystem": "newSubsystem", "detail": "new component"} + outcome = CustomizationReloadOutcome.from_dict(payload) + + assert outcome.status.value == payload["status"] + assert outcome.subsystem.value == payload["subsystem"] + assert outcome.to_dict() == payload + assert outcome.status is CustomizationReloadStatus("newStatus") + assert outcome.subsystem is CustomizationReloadSubsystem("newSubsystem") + assert CustomizationReloadOutcome.from_dict(payload).status is outcome.status + assert CustomizationReloadOutcome.from_dict(payload).subsystem is outcome.subsystem + assert CustomizationReloadStatus("anotherStatus") is not outcome.status + assert CustomizationReloadSubsystem("anotherSubsystem") is not outcome.subsystem + assert CustomizationReloadStatus("reloaded") is CustomizationReloadStatus.RELOADED + assert CustomizationReloadSubsystem("skills") is CustomizationReloadSubsystem.SKILLS + with pytest.raises(ValueError): + CustomizationReloadStatus(None) + + async def test_host_dispose_deserializes_empty_acknowledgement(): client = Mock(request=AsyncMock(return_value={})) diff --git a/python/test_rpc_server_fixture.py b/python/test_rpc_server_fixture.py deleted file mode 100644 index 4467eba68c..0000000000 --- a/python/test_rpc_server_fixture.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Regression controls for the persisted-session E2E fixture's completion fence.""" - -from types import SimpleNamespace -from unittest.mock import AsyncMock - -import pytest - -from copilot.generated.session_events import AssistantMessageData -from copilot.rpc import LocalSessionMetadataValue, SessionContext -from e2e import test_rpc_server_e2e as scenario - - -@pytest.mark.asyncio -@pytest.mark.parametrize("disconnect_fails", [False, True]) -async def test_persisted_session_fixture_observes_completion_before_save_and_cleanup( - tmp_path, monkeypatch, disconnect_fails -): - state = {} - calls = [] - - async def send(*args, **kwargs): - calls.append("send-without-completion") - - async def send_and_wait(prompt, timeout): - assert prompt == "Record a turn for sessions.list discriminator coverage" - assert timeout == 60.0 - calls.append("completed-turn") - return SimpleNamespace( - data=AssistantMessageData( - content=scenario.SYNTHETIC_TEXT, message_id="fixture-assistant" - ) - ) - - async def save(request): - assert calls == ["completed-turn"], "persistence must follow observed turn completion" - assert request.session_id == state["session_id"] - calls.append("save") - return object() - - async def listed(request): - assert calls == ["completed-turn", "save"] - return SimpleNamespace( - sessions=[ - LocalSessionMetadataValue( - session_id=state["session_id"], - is_remote=False, - start_time="2026-01-01T00:00:00Z", - modified_time="2026-01-01T00:00:00Z", - context=SessionContext(cwd=state["working_directory"]), - ) - ] - ) - - async def disconnect(): - calls.append("disconnect") - if disconnect_fails: - raise RuntimeError("controlled detach failure") - - session = SimpleNamespace(send=send, send_and_wait=send_and_wait, disconnect=disconnect) - - async def create_session(**kwargs): - state.update(kwargs) - return session - - client = SimpleNamespace( - start=AsyncMock(), - stop=AsyncMock(), - create_session=create_session, - rpc=SimpleNamespace( - sessions=SimpleNamespace( - save=save, - list=listed, - find_by_prefix=AsyncMock(return_value=SimpleNamespace(session_id=None)), - find_by_task_id=AsyncMock(return_value=SimpleNamespace(session_id=None)), - get_last_for_context=AsyncMock(return_value=SimpleNamespace(session_id=None)), - get_sizes=AsyncMock(return_value=SimpleNamespace(sizes={})), - check_in_use=AsyncMock(return_value=SimpleNamespace(in_use=[])), - ) - ), - ) - - def make_client(ctx, token, **kwargs): - if "request_handler" in kwargs: - assert isinstance(kwargs["request_handler"], scenario._PersistedSessionRequestHandler) - return client - - monkeypatch.setattr(scenario, "_configure_user", AsyncMock()) - monkeypatch.setattr(scenario, "_make_authed_client", make_client) - run = scenario.TestRpcServer().test_should_list_find_and_inspect_persisted_session_state - ctx = SimpleNamespace(work_dir=str(tmp_path)) - if disconnect_fails: - with pytest.raises(RuntimeError, match="controlled detach failure"): - await run(ctx) - else: - await run(ctx) - assert calls == ["completed-turn", "save", "disconnect"] - client.stop.assert_awaited_once() diff --git a/python/test_session.py b/python/test_session.py index d194d68c11..b2a25f53dd 100644 --- a/python/test_session.py +++ b/python/test_session.py @@ -10,6 +10,7 @@ from _session_test_helpers import get_next_event_of_type, wait_for_event from copilot import AgentMessageSource, MessageSource +from copilot._jsonrpc import JsonRpcError from copilot.session import Attachment, CopilotSession from copilot.session_events import ( AssistantMessageData, @@ -58,6 +59,31 @@ def _event(data, event_type: SessionEventType) -> SessionEvent: ) +def _external_tool_request(tool_name: str, request_id: str = "request-1") -> SessionEvent: + return _event( + ExternalToolRequestedData( + request_id=request_id, + session_id="session-1", + tool_call_id=f"{request_id}-tool-call", + tool_name=tool_name, + arguments={}, + ), + SessionEventType.EXTERNAL_TOOL_REQUESTED, + ) + + +async def _wait_for_tool_result(client: Mock, request_id: str) -> dict: + for _ in range(20): + for call in client.request.await_args_list: + if ( + call.args[0] == "session.tools.handlePendingToolCall" + and call.args[1]["requestId"] == request_id + ): + return call.args[1] + await asyncio.sleep(0) + raise AssertionError(f"tool result was not sent for {request_id}") + + @pytest.mark.parametrize("use_send_and_wait", [False, True]) @pytest.mark.asyncio async def test_completion_captures_live_idle_before_send_reply_without_idle_in_history( @@ -496,6 +522,283 @@ async def respond(method, params): await asyncio.gather(pending, return_exceptions=True) +@pytest.mark.asyncio +async def test_set_tools_sends_complete_wire_payload_and_installs_handlers_on_success(): + client = Mock() + client.request = AsyncMock(return_value={}) + session = CopilotSession("session-1", client) + + async def handled(_invocation): + return ToolResult(text_result_for_llm="new result") + + await session.set_tools( + [ + Tool( + name="new_tool", + description="", + parameters={"type": "object"}, + handler=handled, + overrides_built_in_tool=True, + skip_permission=True, + defer="never", + metadata={"owner": "test"}, + is_terminal=True, + ), + Tool(name="declaration_only", description=None), # type: ignore[arg-type] + ] + ) + + client.request.assert_awaited_once_with( + "session.tools.set", + { + "sessionId": "session-1", + "tools": [ + { + "description": "", + "name": "new_tool", + "defer": "never", + "isTerminal": True, + "metadata": {"owner": "test"}, + "overridesBuiltInTool": True, + "parameters": {"type": "object"}, + "skipPermission": True, + }, + {"description": "", "name": "declaration_only"}, + ], + }, + ) + + assert session._get_tool_handler("new_tool") is handled + assert session._get_tool_handler("declaration_only") is None + + +@pytest.mark.asyncio +async def test_set_tools_keeps_previous_handlers_while_rpc_is_pending_then_swaps(): + rpc_started = asyncio.Event() + release_rpc = asyncio.Event() + client = Mock() + + async def request(method, params): + if method == "session.tools.set": + rpc_started.set() + await release_rpc.wait() + return {} + return {"success": True} + + client.request = AsyncMock(side_effect=request) + session = CopilotSession("session-1", client) + calls: list[str] = [] + + async def old_handler(_invocation): + calls.append("old") + return ToolResult(text_result_for_llm="old result") + + async def new_handler(_invocation): + calls.append("new") + return ToolResult(text_result_for_llm="new result") + + session._register_tools([Tool("old_tool", "Old", old_handler)]) + replace = asyncio.create_task(session.set_tools([Tool("new_tool", "New", new_handler)])) + try: + await asyncio.wait_for(rpc_started.wait(), timeout=1) + session._dispatch_event(_external_tool_request("old_tool", "old-request")) + await _wait_for_tool_result(client, "old-request") + assert calls == ["old"] + assert session._get_tool_handler("new_tool") is None + + release_rpc.set() + await asyncio.wait_for(replace, timeout=1) + assert session._get_tool_handler("old_tool") is None + assert session._get_tool_handler("new_tool") is new_handler + + session._dispatch_event(_external_tool_request("new_tool", "new-request")) + await _wait_for_tool_result(client, "new-request") + assert calls == ["old", "new"] + finally: + release_rpc.set() + replace.cancel() + await asyncio.gather(replace, return_exceptions=True) + + +@pytest.mark.asyncio +async def test_set_tools_rejection_leaves_handlers_unchanged(): + client = Mock() + client.request = AsyncMock(side_effect=JsonRpcError(-32602, "invalid params")) + session = CopilotSession("session-1", client) + + async def old_handler(_invocation): + return ToolResult(text_result_for_llm="old result") + + async def new_handler(_invocation): + return ToolResult(text_result_for_llm="new result") + + session._register_tools([Tool("old_tool", "Old", old_handler)]) + + with pytest.raises(JsonRpcError): + await session.set_tools([Tool("new_tool", "New", new_handler)]) + + assert session._get_tool_handler("old_tool") is old_handler + assert session._get_tool_handler("new_tool") is None + + +@pytest.mark.asyncio +async def test_set_tools_empty_list_removes_handlers_after_success(): + client = Mock() + client.request = AsyncMock(return_value={}) + session = CopilotSession("session-1", client) + + async def old_handler(_invocation): + return ToolResult(text_result_for_llm="old result") + + session._register_tools([Tool("old_tool", "Old", old_handler)]) + + await session.set_tools([]) + + client.request.assert_awaited_once_with( + "session.tools.set", {"sessionId": "session-1", "tools": []} + ) + assert session._get_tool_handler("old_tool") is None + + +@pytest.mark.asyncio +async def test_set_tools_serializes_concurrent_calls_and_allows_later_after_failure(): + set_requests: list[dict] = [] + releases = [asyncio.Event(), asyncio.Event()] + client = Mock() + + async def request(method, params): + assert method == "session.tools.set" + index = len(set_requests) + set_requests.append(params) + await releases[index].wait() + if index == 0: + raise JsonRpcError(-32602, "invalid params") + return {} + + client.request = AsyncMock(side_effect=request) + session = CopilotSession("session-1", client) + + async def first_handler(_invocation): + return ToolResult(text_result_for_llm="first") + + async def second_handler(_invocation): + return ToolResult(text_result_for_llm="second") + + first = asyncio.create_task(session.set_tools([Tool("first_tool", "First", first_handler)])) + second = asyncio.create_task(session.set_tools([Tool("second_tool", "Second", second_handler)])) + try: + for _ in range(20): + if len(set_requests) == 1: + break + await asyncio.sleep(0) + assert [request["tools"][0]["name"] for request in set_requests] == ["first_tool"] + + releases[0].set() + with pytest.raises(JsonRpcError): + await asyncio.wait_for(first, timeout=1) + + for _ in range(20): + if len(set_requests) == 2: + break + await asyncio.sleep(0) + assert [request["tools"][0]["name"] for request in set_requests] == [ + "first_tool", + "second_tool", + ] + + releases[1].set() + await asyncio.wait_for(second, timeout=1) + assert session._get_tool_handler("first_tool") is None + assert session._get_tool_handler("second_tool") is second_handler + finally: + for release in releases: + release.set() + first.cancel() + second.cancel() + await asyncio.gather(first, second, return_exceptions=True) + + +@pytest.mark.asyncio +async def test_set_tools_caller_cancellation_after_request_still_installs_accepted_handlers(): + rpc_started = asyncio.Event() + release_rpc = asyncio.Event() + client = Mock() + + async def request(method, params): + assert method == "session.tools.set" + rpc_started.set() + await release_rpc.wait() + return {} + + client.request = AsyncMock(side_effect=request) + session = CopilotSession("session-1", client) + + async def new_handler(_invocation): + return ToolResult(text_result_for_llm="new result") + + replace = asyncio.create_task(session.set_tools([Tool("new_tool", "New", new_handler)])) + try: + await asyncio.wait_for(rpc_started.wait(), timeout=1) + replace.cancel() + with pytest.raises(asyncio.CancelledError): + await replace + + release_rpc.set() + for _ in range(20): + if session._get_tool_handler("new_tool") is new_handler: + break + await asyncio.sleep(0) + assert session._get_tool_handler("new_tool") is new_handler + finally: + release_rpc.set() + + +@pytest.mark.asyncio +async def test_set_tools_cancelled_while_queued_sends_nothing(): + set_requests: list[dict] = [] + release_first = asyncio.Event() + client = Mock() + + async def request(method, params): + assert method == "session.tools.set" + set_requests.append(params) + await release_first.wait() + return {} + + client.request = AsyncMock(side_effect=request) + session = CopilotSession("session-1", client) + + async def first_handler(_invocation): + return ToolResult(text_result_for_llm="first") + + async def queued_handler(_invocation): + return ToolResult(text_result_for_llm="queued") + + first = asyncio.create_task(session.set_tools([Tool("first_tool", "First", first_handler)])) + queued = asyncio.create_task(session.set_tools([Tool("queued_tool", "Queued", queued_handler)])) + try: + for _ in range(20): + if set_requests: + break + await asyncio.sleep(0) + assert len(set_requests) == 1 + + queued.cancel() + with pytest.raises(asyncio.CancelledError): + await queued + + release_first.set() + await asyncio.wait_for(first, timeout=1) + assert [request["tools"][0]["name"] for request in set_requests] == ["first_tool"] + assert session._get_tool_handler("first_tool") is first_handler + assert session._get_tool_handler("queued_tool") is None + finally: + release_first.set() + first.cancel() + queued.cancel() + await asyncio.gather(first, queued, return_exceptions=True) + + @pytest.mark.asyncio async def test_external_tool_completed_cancels_blocked_handler(): client = Mock() diff --git a/python/test_session_fs_provider.py b/python/test_session_fs_provider.py new file mode 100644 index 0000000000..462abd6b9e --- /dev/null +++ b/python/test_session_fs_provider.py @@ -0,0 +1,41 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# -------------------------------------------------------------------------------------------- + +"""Unit tests for the session filesystem provider adapter.""" + +from types import SimpleNamespace +from unittest.mock import AsyncMock, Mock + +import pytest + +from copilot import SessionFsWriteFailure +from copilot.session_fs_provider import SessionFsProvider, create_session_fs_adapter + + +@pytest.mark.parametrize( + ("failure", "changed"), + [(RuntimeError("rejected"), None), (SessionFsWriteFailure("disk full"), True)], +) +async def test_failed_write_reports_only_known_changes(failure: Exception, changed: bool | None): + provider = Mock(spec=SessionFsProvider) + provider.write_file = AsyncMock(side_effect=failure) + adapter = create_session_fs_adapter(provider) + + error = await adapter.write_file(SimpleNamespace(path="/file", content="data", mode=None)) + + assert error is not None + assert error.message == str(failure) + assert error.write_changed is changed + assert error.to_dict().get("writeChanged") is changed + + +async def test_write_marker_does_not_leak_into_other_methods(): + provider = Mock(spec=SessionFsProvider) + provider.read_file = AsyncMock(side_effect=SessionFsWriteFailure("read failed")) + adapter = create_session_fs_adapter(provider) + + result = await adapter.read_file(SimpleNamespace(path="/file")) + + assert result.error is not None + assert result.error.write_changed is None diff --git a/python/test_subagent_hooks.py b/python/test_subagent_hooks.py new file mode 100644 index 0000000000..71db1c6c00 --- /dev/null +++ b/python/test_subagent_hooks.py @@ -0,0 +1,130 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + +from datetime import UTC, datetime + +import pytest + +from copilot import ( + SessionHooks, + SubagentStartHookInput, + SubagentStartHookOutput, + SubagentStopHookInput, + SubagentStopHookOutput, +) +from copilot.session import CopilotSession + + +@pytest.mark.asyncio +async def test_subagent_start_dispatches_normalized_input_and_context(): + captured: list[tuple[SubagentStartHookInput, dict[str, str]]] = [] + + async def on_start( + input_data: SubagentStartHookInput, invocation: dict[str, str] + ) -> SubagentStartHookOutput: + captured.append((input_data, invocation)) + return {"additionalContext": "Report the entire file"} + + hooks: SessionHooks = {"on_subagent_start": on_start} + session = CopilotSession("parent", None) + session._register_hooks(hooks) + + output = await session._handle_hooks_invoke( + "subagentStart", + { + "sessionId": "parent", + "timestamp": 1_700_000_000_000, + "cwd": "work", + "transcriptPath": "transcript.jsonl", + "agentName": "explore", + "agentDisplayName": "Explorer", + "agentDescription": "Reads files", + }, + ) + + assert output == {"additionalContext": "Report the entire file"} + assert captured == [ + ( + { + "sessionId": "parent", + "timestamp": datetime.fromtimestamp(1_700_000_000, tz=UTC), + "workingDirectory": "work", + "transcriptPath": "transcript.jsonl", + "agentName": "explore", + "agentDisplayName": "Explorer", + "agentDescription": "Reads files", + }, + {"session_id": "parent"}, + ) + ] + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "hook_output", + [ + {"decision": "block", "reason": "Read the rest of the file"}, + {"modifiedResponse": "Verified: Hello from subagent test!"}, + {"decision": "allow", "modifiedResponse": "Verified: Hello from subagent test!"}, + ], +) +async def test_subagent_stop_dispatches_normalized_input_and_verdict( + hook_output: SubagentStopHookOutput, +): + captured: list[tuple[SubagentStopHookInput, dict[str, str]]] = [] + + def on_stop( + input_data: SubagentStopHookInput, invocation: dict[str, str] + ) -> SubagentStopHookOutput: + captured.append((input_data, invocation)) + return hook_output + + hooks: SessionHooks = {"on_subagent_stop": on_stop} + session = CopilotSession("parent", None) + session._register_hooks(hooks) + + output = await session._handle_hooks_invoke( + "subagentStop", + { + "sessionId": "parent", + "timestamp": 1_700_000_000_000, + "cwd": "work", + "transcriptPath": "transcript.jsonl", + "agentName": "explore", + "agentType": "explore", + "stopReason": "end_turn", + "response": "Hello from subagent test!", + }, + ) + + assert output == hook_output + assert captured == [ + ( + { + "sessionId": "parent", + "timestamp": datetime.fromtimestamp(1_700_000_000, tz=UTC), + "workingDirectory": "work", + "transcriptPath": "transcript.jsonl", + "agentName": "explore", + "agentType": "explore", + "stopReason": "end_turn", + "response": "Hello from subagent test!", + }, + {"session_id": "parent"}, + ) + ] + + +@pytest.mark.asyncio +async def test_subagent_stop_without_handler_returns_none(): + session = CopilotSession("parent", None) + session._register_hooks({"on_subagent_start": lambda _input, _invocation: None}) + + assert await session._handle_hooks_invoke("subagentStop", {}) is None + + +@pytest.mark.asyncio +async def test_subagent_start_without_handler_returns_none(): + session = CopilotSession("parent", None) + session._register_hooks({"on_subagent_stop": lambda _input, _invocation: None}) + + assert await session._handle_hooks_invoke("subagentStart", {}) is None diff --git a/python/test_tools.py b/python/test_tools.py index 031f68c053..5d8f6043dd 100644 --- a/python/test_tools.py +++ b/python/test_tools.py @@ -1,9 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + """Unit tests for define_tool""" import json import pytest -from pydantic import BaseModel, ConfigDict, Field, field_validator +from pydantic import BaseModel, ConfigDict, Field, RootModel, field_validator from copilot import define_tool from copilot.generated.rpc import ExternalToolTextResultForLlm @@ -18,6 +20,27 @@ class TestDefineTool: + @pytest.mark.parametrize("patch", ["", "*** Begin Patch\n*** End Patch"]) + async def test_string_schema_handler_preserves_scalar_arguments(self, patch: str): + calls = [] + + @define_tool("apply_patch", description="Apply a patch", overrides_built_in_tool=True) + def apply_patch(params: RootModel[str], invocation: ToolInvocation) -> str: + calls.append((params.root, invocation.arguments)) + return "accepted" + + result = await apply_patch.handler( + ToolInvocation( + session_id="session-1", + tool_call_id="call-1", + tool_name="apply_patch", + arguments=patch, + ) + ) + + assert result.result_type == "success", result.error + assert calls == [(patch, patch)] + def test_creates_tool_with_correct_name_and_description(self): class Params(BaseModel): query: str diff --git a/python/tests/test_installation_confirmation.py b/python/tests/test_installation_confirmation.py index 92db520c67..8056ca027b 100644 --- a/python/tests/test_installation_confirmation.py +++ b/python/tests/test_installation_confirmation.py @@ -2,6 +2,7 @@ import asyncio import os +import threading from typing import Any, cast import pytest @@ -83,6 +84,9 @@ async def close_peer_connection(self) -> None: await asyncio.sleep(0) async def close(self) -> None: + # Unblock both readers before closing either read descriptor (Windows CRT). + self.peer_process.stdin.close() + self.sdk_process.stdin.close() self.peer_process.terminate() self.sdk_process.terminate() await self.peer_rpc.stop() @@ -180,6 +184,50 @@ async def _wait_for(predicate, *, timeout: float = 5.0) -> None: await asyncio.sleep(0) +async def test_closes_both_writers_before_waiting_for_pipe_readers(monkeypatch) -> None: + loop = asyncio.get_running_loop() + reading = asyncio.Event() + sdk_process, peer_process = _pipe_process_pair() + reader_lock = threading.Lock() + original_readline = peer_process.stdout.readline + original_close = peer_process.stdout.close + + # Windows' CRT holds the descriptor lock across a blocking read and takes + # that same lock on close. Reproduce that boundary on every platform. + def locked_readline(): + with reader_lock: + loop.call_soon_threadsafe(reading.set) + return original_readline() + + def locked_close(): + with reader_lock: + original_close() + + monkeypatch.setattr(peer_process.stdout, "readline", locked_readline) + monkeypatch.setattr(peer_process.stdout, "close", locked_close) + monkeypatch.setattr(f"{__name__}._pipe_process_pair", lambda: (sdk_process, peer_process)) + harness = await _connect() + closing = None + try: + await asyncio.wait_for(reading.wait(), timeout=5) + closing = asyncio.create_task(asyncio.to_thread(lambda: asyncio.run(harness.close()))) + await asyncio.wait_for(asyncio.shield(closing), timeout=5) + for rpc in (harness.peer_rpc, harness.sdk_rpc): + assert rpc._read_thread is not None # noqa: SLF001 + assert not rpc._read_thread.is_alive() # noqa: SLF001 + for process in (sdk_process, peer_process): + assert process.stdin.closed + assert process.stdout.closed + finally: + # Release a deadlocked close before awaiting its cleanup on failure. + sdk_process.stdin.close() + peer_process.stdin.close() + if closing is not None: + await closing + else: + await harness.close() + + async def test_presents_typed_full_review_and_echoes_challenge_and_fingerprint() -> None: seen: list[tuple[InstallationConfirmationRequest, InstallationConfirmationContext]] = [] @@ -231,7 +279,7 @@ def handler( second = asyncio.create_task( harness.peer.request(402, "installations.confirm", _request("b")) ) - await _wait_for(lambda: seen == ["a", "b"]) + await _wait_for(lambda: sorted(seen) == ["a", "b"]) with pytest.raises(JsonRpcError, match="No GitHub token provider"): await harness.peer.request( diff --git a/rust/AGENTS.md b/rust/AGENTS.md index a7509e57d4..de40a114f8 100644 --- a/rust/AGENTS.md +++ b/rust/AGENTS.md @@ -100,9 +100,13 @@ cargo check --no-default-features --features local-runtime Run it with `COPILOT_SKIP_CLI_DOWNLOAD` unset. Tests that start the runtime must also set `COPILOT_CLI_PATH` to the prepared same-checkout wrapper. -Follow the SDK's existing crate-local unit-test and `tests/` integration-test -layout. Integration tests needing SDK test helpers use the `test-support` -feature because the library is compiled without `cfg(test)`. +Keep unit tests in external test-only modules: `foo.rs` declares +`#[cfg(test)] mod tests;`, and its tests live in `foo/tests.rs`. Crate roots +such as `lib.rs` use a sibling `tests.rs`; descriptive module names keep their +corresponding file names. Start each test-only file with `#![cfg(test)]`. +Keep integration tests in `tests/`. Integration tests needing SDK test helpers +use the `test-support` feature because the library is compiled without +`cfg(test)`. Default features bundle the CLI; disabling them changes what must be provided externally. For transport-specific E2E settings, bundled-runtime checks, and diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 0b5c85e472..f8d88de1c1 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,3 +1,5 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + [package] name = "github-copilot-sdk" version = "0.0.0-dev" @@ -44,6 +46,10 @@ test-support = [] all-features = true rustdoc-args = ["--cfg", "docsrs"] +[lints.clippy] +# Threshold lives in clippy.toml; consumers run on 2 MiB Tokio worker stacks. +large_futures = "warn" + [dependencies] async-trait = "0.1" indexmap = { version = "2", features = ["serde"] } @@ -81,15 +87,20 @@ windows-sys = { version = "0.61", default-features = false, features = [ ] } [dev-dependencies] -ureq = { version = "3", default-features = false, features = ["native-tls"] } +flate2 = "1" +native-tls = "0.2" ahp = { git = "https://github.com/microsoft/agent-host-protocol", tag = "rust/v0.9.0" } ahp-types = { git = "https://github.com/microsoft/agent-host-protocol", tag = "rust/v0.9.0" } ahp-ws = { git = "https://github.com/microsoft/agent-host-protocol", tag = "rust/v0.9.0", default-features = false } rusqlite = { version = "0.40", features = ["bundled"] } schemars = "1" serial_test = "4" +sha2 = "0.10" +tar = "0.4" tempfile = "3" tokio = { version = "1", features = ["rt-multi-thread"] } +ureq = { version = "3", default-features = false, features = ["native-tls"] } +zip = { version = "7.2.0", default-features = false, features = ["deflate"] } # Integration tests that call test-support-only Client methods (e.g. # `from_streams_with_connection_token`, `from_streams_with_trace_provider`) @@ -133,6 +144,13 @@ required-features = ["test-support"] test = false bench = false +[[bin]] +name = "copilot-connectors-runtime-fixture" +path = "tests/fixtures/connector-runtime/src/main.rs" +required-features = ["test-support", "in-process"] +test = false +bench = false + [[test]] name = "prepared_session_test" required-features = ["test-support"] diff --git a/rust/README.md b/rust/README.md index 17f6d6a570..7b3d28cde6 100644 --- a/rust/README.md +++ b/rust/README.md @@ -1,3 +1,5 @@ + + # 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 @@ -143,27 +152,32 @@ use github_copilot_sdk::{AhpHostOptions, Client}; # async fn example(client: &Client) -> Result<(), github_copilot_sdk::Error> { let host = client.start_ahp_host( AhpHostOptions::default() - .with_hostname("127.0.0.1") - .with_port(0) + .with_local_server(Default::default()) .with_on_exit(|exit| { println!("host {} exited: {:?}", exit.host_id, exit.reason); }), ).await?; -println!("{} (in-process host {})", host.url, host.host_id); +println!("{:?} (in-process host {})", host.url, host.host_id); // Supply host.token to AHP clients when present; never log it. host.dispose().await?; # Ok(()) # } ``` -`AhpHostOptions` has optional `hostname`, `port`, `token`, -`require_connection_token`, and local-only `on_exit` fields, with matching -`with_*` builders. Defaults and validation stay in the runtime: loopback hostname, -an available port, and required connection-token authentication. Set -`with_require_connection_token(false)` to disable token authentication; then -`AhpHost::token` is `None`. - -The returned `AhpHost` exposes `host_id`, `url`, `pid`, and `token`. +`AhpHostOptions` requires at least one explicit transport: `with_local_server` +accepts generated `rpc::HostLocalServerOptions`, and `with_github_environment` +accepts generated `rpc::HostGitHubEnvironmentOptions` with required `name` and +`compute_id`. Both transports may be enabled. There is no implicit local listener. +Local `hostname`, `port`, `token`, and `require_connection_token` settings belong +inside `HostLocalServerOptions`. Its runtime defaults are loopback, an available +port, and required token authentication. Set its `require_connection_token` to +`Some(false)` to disable connection-token authentication. +The `on_exit` callback remains local-only. All hosting APIs are experimental. + +The returned `AhpHost` exposes `host_id` and optional `url`, `pid`, `token`, and +`environment_id`. Mission Control-only hosting has no local URL; `environment_id` +identifies its registration. Environment list/get/delete operations are available +only through the generated `client.rpc().environments()` namespace. `pid` is `None` for in-process listeners; `Some(pid)` preserves a separate host process ID returned by a legacy runtime, never the runtime PID. Stop the in-process listener with `dispose()`. There is no @@ -209,6 +223,7 @@ use github_copilot_sdk::handler::ApproveAllHandler; # async fn example(client: &Client) -> Result<(), github_copilot_sdk::Error> { let host = client.start_ahp_host( AhpHostOptions::new() + .with_local_server(Default::default()) .with_create_session(|request: AhpSessionRequest, client: Client| async move { // Use this request-scoped client instead of capturing the owner. // Preserve request.config; add your tools/hooks here. @@ -768,12 +783,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; @@ -880,6 +897,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. @@ -1314,6 +1369,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 @@ -1445,7 +1510,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 @@ -1478,6 +1557,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.): @@ -1569,9 +1652,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/clippy.toml b/rust/clippy.toml index 22781c4721..a9912b0514 100644 --- a/rust/clippy.toml +++ b/rust/clippy.toml @@ -6,3 +6,7 @@ await-holding-invalid-types = [ disallowed-macros = [ { path = "tracing::instrument", reason = "tracing::instrument is error-prone. Use tracing::error_span! in the method body instead." }, ] + +# Matches the GitHub Copilot app, which vendors this crate and runs on 2 MiB +# Tokio worker stacks; enforced through `large_futures` in Cargo.toml. +future-size-threshold = 16384 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 bbf60ff0eb..2b42b86ff0 100644 --- a/rust/src/ahp_host.rs +++ b/rust/src/ahp_host.rs @@ -141,18 +141,15 @@ pub type AhpSessionReleasedCallback = Arc) + Send + Sync>; /// Options for [`Client::start_ahp_host`]. /// /// **Experimental.** May change or be removed in future releases. -/// Listener defaults and validation belong to the runtime, not the SDK. +/// At least one transport must be explicitly enabled; both may be used. +/// Transport defaults and validation belong to the runtime. #[derive(Clone, Default)] #[non_exhaustive] pub struct AhpHostOptions { - /// Listener hostname; defaults to loopback in the runtime. - pub hostname: Option, - /// Listener port; zero asks the runtime for an available port. - pub port: Option, - /// Optional explicit connection token. - pub token: Option, - /// Whether connections require a token. Defaults to true in the runtime. - pub require_connection_token: Option, + /// Experimental local WebSocket listener configuration. + pub local_server: Option, + /// Experimental Mission Control registration; name and compute ID are required. + pub github_environment: Option, /// Local callback, never serialized. Called at most once. /// /// Disconnect reports `OwnerDisconnected` without claiming the host was @@ -174,10 +171,11 @@ pub struct AhpHostOptions { impl std::fmt::Debug for AhpHostOptions { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("AhpHostOptions") - .field("hostname", &self.hostname) - .field("port", &self.port) - .field("token", &self.token.as_ref().map(|_| "[redacted]")) - .field("require_connection_token", &self.require_connection_token) + .field( + "local_server", + &self.local_server.as_ref().map(|_| "[configured]"), + ) + .field("github_environment", &self.github_environment) .field("on_exit", &self.on_exit.is_some()) .field("create_session", &self.create_session.is_some()) .field("resume_session", &self.resume_session.is_some()) @@ -187,32 +185,23 @@ impl std::fmt::Debug for AhpHostOptions { } impl AhpHostOptions { - /// Create options using runtime defaults. + /// Create options with no transports enabled. Select at least one before starting. pub fn new() -> Self { Self::default() } - /// Set the listener hostname. - pub fn with_hostname(mut self, hostname: impl Into) -> Self { - self.hostname = Some(hostname.into()); - self - } - - /// Set the listener port. - pub fn with_port(mut self, port: i32) -> Self { - self.port = Some(port); - self - } - - /// Set an explicit connection token. - pub fn with_token(mut self, token: impl Into) -> Self { - self.token = Some(token.into()); + /// Enable the local WebSocket listener. Experimental; may change or be removed. + pub fn with_local_server(mut self, options: crate::rpc::HostLocalServerOptions) -> Self { + self.local_server = Some(options); self } - /// Set whether connections require a token. - pub fn with_require_connection_token(mut self, required: bool) -> Self { - self.require_connection_token = Some(required); + /// Enable Mission Control registration. Experimental; may change or be removed. + pub fn with_github_environment( + mut self, + options: crate::rpc::HostGitHubEnvironmentOptions, + ) -> Self { + self.github_environment = Some(options); self } @@ -236,6 +225,7 @@ impl AhpHostOptions { /// # async fn example(owner: &Client) -> Result<(), github_copilot_sdk::Error> { /// let host = owner.start_ahp_host( /// AhpHostOptions::new() + /// .with_local_server(Default::default()) /// .with_create_session(|request: AhpSessionRequest, client: Client| async move { /// // Add application tools and hooks without changing host settings. /// Ok(Arc::new(client.create_session(request.config).await?)) @@ -286,8 +276,10 @@ impl AhpHostOptions { pub struct AhpHost { /// Runtime host identifier. pub host_id: String, - /// Listener URL returned by the runtime. - pub url: String, + /// Local listener URL, absent for Mission Control-only hosts. + pub url: Option, + /// Experimental Mission Control environment identifier, when registered. + pub environment_id: Option, /// Legacy separate host process ID. Absent for in-process listeners; use `dispose()` to stop. pub pid: Option, /// Connection token, absent when token authentication is disabled. @@ -300,6 +292,7 @@ impl std::fmt::Debug for AhpHost { f.debug_struct("AhpHost") .field("host_id", &self.host_id) .field("url", &self.url) + .field("environment_id", &self.environment_id) .field("pid", &self.pid) .field("token", &self.token.as_ref().map(|_| "[redacted]")) .finish_non_exhaustive() @@ -412,6 +405,12 @@ impl Client { /// Cancelling this future removes local callbacks and disposes a successfully /// started listener once the pending start response arrives. pub async fn start_ahp_host(&self, options: AhpHostOptions) -> Result { + if options.local_server.is_none() && options.github_environment.is_none() { + return Err(Error::with_message( + ErrorKind::InvalidConfig, + "AHP hosting requires localServer or githubEnvironment", + )); + } let host_id = uuid::Uuid::new_v4().to_string(); let mut pending = PendingStart { callbacks: self.inner.ahp_host_callbacks.clone(), @@ -439,10 +438,8 @@ impl Client { "host.start", Some(serde_json::to_value(HostStartRequest { host_id, - hostname: options.hostname, - port: options.port, - token: options.token, - require_connection_token: options.require_connection_token, + local_server: options.local_server, + github_environment: options.github_environment, session_factory, resume_factory, })?), @@ -468,6 +465,7 @@ impl Client { Ok(AhpHost { host_id: result.host_id, url: result.url, + environment_id: result.environment_id, pid: result.pid, token: result.token, client: Arc::downgrade(&self.inner), @@ -960,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")), } @@ -990,6 +989,9 @@ pub(crate) fn resume_config_for_host(config: &ResumeSessionConfig) -> Result ( ) { let (created_tx, created) = mpsc::unbounded_channel(); let (released_tx, released) = mpsc::unbounded_channel(); - let options = AhpHostOptions::new() + let options = local_options() .with_create_session(move |request: AhpSessionRequest, client: Client| { let created_tx = created_tx.clone(); async move { @@ -63,7 +63,7 @@ async fn owner_loss_cancels_pending_factory_but_releases_its_late_original() { let (released_tx, mut released) = mpsc::unbounded_channel(); let finish = CancellationToken::new(); let gate = finish.clone(); - let options = AhpHostOptions::new() + let options = local_options() .with_create_session(move |request: AhpSessionRequest, client: Client| { let ready_tx = ready_tx.clone(); let gate = gate.clone(); @@ -128,7 +128,7 @@ async fn resume_can_return_retained_original_after_creation_handoff_released() { let (released_tx, mut resumed_released) = mpsc::unbounded_channel(); let pending = start( &client, - AhpHostOptions::new() + local_options() .with_resume_session(move |_: AhpSessionResumeRequest, _: Client| { let retained = retained.clone(); async move { Ok(retained) } @@ -191,7 +191,7 @@ async fn resume_rejects_superseded_retained_original() { let (released_tx, mut resumed_released) = mpsc::unbounded_channel(); let pending = start( &client, - AhpHostOptions::new() + local_options() .with_resume_session(move |_: AhpSessionResumeRequest, _: Client| { let retained = retained.clone(); async move { Ok(retained) } @@ -232,7 +232,7 @@ async fn resume_factory_registered_before_start_retains_original_and_preserves_s let (released_tx, mut released) = mpsc::unbounded_channel(); let pending = start( &client, - AhpHostOptions::new() + local_options() .with_resume_session(move |request: AhpSessionResumeRequest, client: Client| { let created_tx = created_tx.clone(); async move { @@ -289,10 +289,9 @@ async fn resume_factory_registered_before_start_retains_original_and_preserves_s #[tokio::test] async fn resume_never_falls_back_to_create_factory() { let (client, mut peer) = fixture(); - let options = - AhpHostOptions::new().with_create_session(|_: AhpSessionRequest, _: Client| async { - panic!("must not create on resume") - }); + let options = local_options().with_create_session(|_: AhpSessionRequest, _: Client| async { + panic!("must not create on resume") + }); let host = start_factory(&client, &mut peer, options).await; peer.send(json!({ "jsonrpc": "2.0", "id": 900, "method": "host.materializeSession", @@ -317,7 +316,7 @@ async fn cancelled_resume_releases_late_original_once() { let (released_tx, mut released) = mpsc::unbounded_channel(); let finish = CancellationToken::new(); let gate = finish.clone(); - let options = AhpHostOptions::new() + let options = local_options() .with_resume_session(move |request: AhpSessionResumeRequest, client: Client| { let ready_tx = ready_tx.clone(); let gate = gate.clone(); @@ -416,7 +415,7 @@ async fn cancellation_responds_promptly_and_releases_late_original_once() { let (released_tx, mut released) = mpsc::unbounded_channel(); let finish = CancellationToken::new(); let gate = finish.clone(); - let options = AhpHostOptions::new() + let options = local_options() .with_create_session(move |request: AhpSessionRequest, client: Client| { let ready_tx = ready_tx.clone(); let gate = gate.clone(); @@ -454,7 +453,7 @@ async fn cancellation_responds_promptly_and_releases_late_original_once() { async fn callback_failure_and_panic_fail_handoff_without_release() { for panic in [false, true] { let (client, mut peer) = fixture(); - let options = AhpHostOptions::new().with_create_session( + let options = local_options().with_create_session( move |_: AhpSessionRequest, _: Client| async move { assert!(!panic, "application panic"); Err(handoff_error("application failure")) @@ -473,7 +472,7 @@ async fn callback_failure_and_panic_fail_handoff_without_release() { async fn altered_post_create_setting_is_rejected_and_original_released() { let (client, mut peer) = fixture(); let (released_tx, mut released) = mpsc::unbounded_channel(); - let options = AhpHostOptions::new() + let options = local_options() .with_create_session( |mut request: AhpSessionRequest, client: Client| async move { request.config.skip_custom_instructions = None; @@ -602,7 +601,10 @@ fn host_settings_round_trip_without_callbacks_or_losing_constraints() { Some(false) ); assert_eq!(config_for_host(&config).unwrap(), value); - let resume = resume_config_from_host(&serde_json::from_value(value.clone()).unwrap()).unwrap(); + let mut resume_value = value.clone(); + resume_value["allowTranscriptRecovery"] = json!(false); + let resume = + resume_config_from_host(&serde_json::from_value(resume_value.clone()).unwrap()).unwrap(); assert!(resume.permission_handler.is_none()); assert_eq!(resume.config_directory, Some(PathBuf::from("/config"))); assert_eq!(resume.enable_experimental_mode, Some(true)); @@ -610,7 +612,14 @@ fn host_settings_round_trip_without_callbacks_or_losing_constraints() { resume.infinite_sessions.as_ref().unwrap().enabled, Some(false) ); - assert_eq!(resume_config_for_host(&resume).unwrap(), value); + assert_eq!(resume_config_for_host(&resume).unwrap(), resume_value); + assert_eq!(resume.allow_transcript_recovery, Some(false)); + let explicit_true = crate::ResumeSessionConfig::new(crate::SessionId::from("id")) + .with_allow_transcript_recovery(true); + assert_eq!( + resume_config_for_host(&explicit_true).unwrap()["allowTranscriptRecovery"], + true + ); assert!(config_from_host(&serde_json::from_value(json!({"unknown":true})).unwrap()).is_err()); assert!(contains_settings( Some(&json!({"a":{"b":true,"c":1}})), @@ -694,7 +703,7 @@ async fn callback_returning_another_clients_session_is_released_not_destroyed() let host = start_factory( &client, &mut peer, - AhpHostOptions::new() + local_options() .with_create_session(move |_: AhpSessionRequest, _: Client| { let session = session.clone(); async move { Ok(session) } diff --git a/rust/src/ahp_host/lifetime_tests.rs b/rust/src/ahp_host/lifetime_tests.rs index e09abd9056..e4699008d3 100644 --- a/rust/src/ahp_host/lifetime_tests.rs +++ b/rust/src/ahp_host/lifetime_tests.rs @@ -140,7 +140,7 @@ async fn blocked_release_callback_cannot_lose_another_release_behind_notificatio let (blocked_tx, mut blocked) = mpsc::unbounded_channel(); let (unblock, gate) = std::sync::mpsc::channel::<()>(); let gate = std::sync::Mutex::new(gate); - let options = AhpHostOptions::new() + let options = local_options() .with_create_session(move |request: AhpSessionRequest, client: Client| { let created_tx = created_tx.clone(); async move { diff --git a/rust/src/ahp_host/tests.rs b/rust/src/ahp_host/tests.rs index 045a36f293..90f7ed55df 100644 --- a/rust/src/ahp_host/tests.rs +++ b/rust/src/ahp_host/tests.rs @@ -8,6 +8,11 @@ use tokio::sync::mpsc; use tokio::time::timeout; use super::*; +use crate::generated::api_types::{HostGitHubEnvironmentOptions, HostLocalServerOptions}; + +fn local_options() -> AhpHostOptions { + AhpHostOptions::new().with_local_server(HostLocalServerOptions::default()) +} const TIMEOUT: Duration = Duration::from_secs(5); @@ -95,24 +100,77 @@ fn start( fn callback_options() -> (AhpHostOptions, mpsc::UnboundedReceiver) { let (tx, rx) = mpsc::unbounded_channel(); ( - AhpHostOptions::default().with_on_exit(move |exit| { + local_options().with_on_exit(move |exit| { let _ = tx.send(exit); }), rx, ) } +#[tokio::test] +async fn requires_an_explicit_transport() { + let (client, _peer) = fixture(); + let error = client + .start_ahp_host(AhpHostOptions::default()) + .await + .unwrap_err(); + assert!(matches!(error.kind(), ErrorKind::InvalidConfig)); + assert!( + error + .to_string() + .contains("requires localServer or githubEnvironment") + ); + assert!(client.inner.ahp_host_callbacks.lock().is_empty()); +} + +#[tokio::test] +async fn github_and_combined_transports_preserve_optional_results() { + for local in [false, true] { + let (client, mut peer) = fixture(); + let mut options = + AhpHostOptions::new().with_github_environment(HostGitHubEnvironmentOptions { + name: "SDK host".into(), + compute_id: "compute".into(), + }); + if local { + options = options.with_local_server(Default::default()); + } + let pending = start(&client, options); + let request = peer.request().await; + let mut expected = json!({ + "hostId": request["params"]["hostId"], + "githubEnvironment": {"name": "SDK host", "computeId": "compute"} + }); + let mut result = json!({ + "hostId": request["params"]["hostId"], + "environmentId": "environment" + }); + if local { + expected["localServer"] = json!({}); + result["url"] = json!("ws://127.0.0.1:4321"); + } + assert_eq!(request["params"], expected); + peer.respond(&request, result).await; + let host = pending.await.unwrap().unwrap(); + assert_eq!(host.environment_id.as_deref(), Some("environment")); + assert_eq!(host.url.as_deref(), local.then_some("ws://127.0.0.1:4321")); + assert_eq!(host.token, None); + assert_eq!(host.pid, None); + } +} + #[tokio::test] async fn forwards_only_generated_options_and_returns_runtime_fields() { let (client, mut peer) = fixture(); let (options, _exits) = callback_options(); let pending = start( &client, - options - .with_hostname("::1") - .with_port(0) - .with_token("explicit-token") - .with_require_connection_token(true), + options.with_local_server(HostLocalServerOptions { + hostname: Some("::1".into()), + port: Some(0), + token: Some("explicit-token".into()), + require_connection_token: Some(true), + }), ); let request = peer.request().await; assert_eq!(request["method"], "host.start"); @@ -121,22 +179,25 @@ async fn forwards_only_generated_options_and_returns_runtime_fields() { assert_eq!( request["params"], json!({ - "hostId": host_id, "hostname": "::1", "port": 0, - "token": "explicit-token", "requireConnectionToken": true + "hostId": host_id, "localServer": { + "hostname": "::1", "port": 0, + "token": "explicit-token", "requireConnectionToken": true + } }) ); peer.started(&request, Some("runtime-token")).await; let host = pending.await.unwrap().unwrap(); assert_eq!(host.host_id, host_id); assert_eq!(host.pid, None); - assert_eq!(host.url, "http://127.0.0.1:4321"); + assert_eq!(host.url.as_deref(), Some("http://127.0.0.1:4321")); + assert_eq!(host.environment_id, None); assert_eq!(host.token.as_deref(), Some("runtime-token")); } #[tokio::test] async fn preserves_optional_legacy_separate_host_pid() { let (client, mut peer) = fixture(); - let pending = start(&client, AhpHostOptions::default()); + let pending = start(&client, local_options()); let request = peer.request().await; peer.respond( &request, @@ -169,11 +230,12 @@ async fn listener_task_exit_has_no_process_exit_code() { } #[tokio::test] -async fn default_options_are_omitted_and_token_can_be_absent() { +async fn explicit_local_defaults_are_forwarded_and_token_can_be_absent() { let (client, mut peer) = fixture(); - let pending = start(&client, AhpHostOptions::default()); + let pending = start(&client, local_options()); let request = peer.request().await; - assert_eq!(request["params"].as_object().unwrap().len(), 1); + assert_eq!(request["params"].as_object().unwrap().len(), 2); + assert_eq!(request["params"]["localServer"], json!({})); peer.started(&request, None).await; assert!(pending.await.unwrap().unwrap().token.is_none()); assert!(client.inner.ahp_host_callbacks.lock().is_empty()); @@ -182,7 +244,7 @@ async fn default_options_are_omitted_and_token_can_be_absent() { #[tokio::test] async fn publication_forwards_existing_identity_to_the_owning_listener() { let (client, mut peer) = fixture(); - let pending = start(&client, AhpHostOptions::default()); + let pending = start(&client, local_options()); let request = peer.request().await; peer.started(&request, None).await; let host = pending.await.unwrap().unwrap(); @@ -209,17 +271,21 @@ async fn listener_validation_is_left_to_the_runtime() { let (client, mut peer) = fixture(); let pending = start( &client, - AhpHostOptions::default() - .with_hostname("") - .with_port(-1) - .with_token("") - .with_require_connection_token(false), + AhpHostOptions::default().with_local_server(HostLocalServerOptions { + hostname: Some("".into()), + port: Some(-1), + token: Some("".into()), + require_connection_token: Some(false), + }), ); let request = peer.request().await; - assert_eq!(request["params"]["port"], -1); - assert_eq!(request["params"]["hostname"], ""); - assert_eq!(request["params"]["token"], ""); - assert_eq!(request["params"]["requireConnectionToken"], false); + assert_eq!(request["params"]["localServer"]["port"], -1); + assert_eq!(request["params"]["localServer"]["hostname"], ""); + assert_eq!(request["params"]["localServer"]["token"], ""); + assert_eq!( + request["params"]["localServer"]["requireConnectionToken"], + false + ); peer.send(json!({ "jsonrpc": "2.0", "id": request["id"], "error": {"code": -32602, "message": "invalid listener"} @@ -331,7 +397,7 @@ async fn cancelled_start_disposes_after_late_success() { #[tokio::test] async fn cancelled_start_does_not_dispose_after_late_failure() { let (client, mut peer) = fixture(); - let pending = start(&client, AhpHostOptions::new()); + let pending = start(&client, local_options()); let request = peer.request().await; pending.abort(); assert!(pending.await.unwrap_err().is_cancelled()); @@ -350,7 +416,7 @@ async fn cancelled_start_does_not_dispose_after_late_failure() { #[tokio::test] async fn start_future_dropped_on_plain_thread_still_disposes() { let (client, mut peer) = fixture(); - let mut pending = Box::pin(client.start_ahp_host(AhpHostOptions::new())); + let mut pending = Box::pin(client.start_ahp_host(local_options())); assert!(futures_util::poll!(&mut pending).is_pending()); let request = peer.request().await; std::thread::scope(|scope| scope.spawn(move || drop(pending)).join().unwrap()); @@ -365,7 +431,7 @@ async fn start_future_dropped_on_plain_thread_still_disposes() { async fn cancelled_start_does_not_keep_owner_alive_while_waiting_for_response() { let (client, mut peer) = fixture(); let owner = Arc::downgrade(&client.inner); - let pending = start(&client, AhpHostOptions::new()); + let pending = start(&client, local_options()); peer.request().await; pending.abort(); assert!(pending.await.unwrap_err().is_cancelled()); @@ -553,9 +619,10 @@ async fn disconnect_notifies_all_callbacks_even_when_they_panic() { let tx = tx.clone(); let pending = start( &client, - AhpHostOptions::new().with_on_exit(move |exit| { + local_options().with_on_exit(move |exit| { tx.send(exit).unwrap(); - panic!("test disconnect callback panic"); + // Exercise unwinding without blocking channel closure on panic-hook output. + std::panic::resume_unwind(Box::new("test disconnect callback panic")); }), ); let request = peer.request().await; @@ -577,13 +644,20 @@ async fn disconnect_notifies_all_callbacks_even_when_they_panic() { async fn callback_panic_does_not_break_other_callbacks_or_rpc() { let (client, mut peer) = fixture(); let calls = Arc::new(AtomicUsize::new(0)); + let (first_tx, mut first_exits) = mpsc::unbounded_channel(); + // The second callback must not act as a completion barrier for the first. + let (unblock, gate) = std::sync::mpsc::channel::<()>(); + let gate = std::sync::Mutex::new(gate); let pending = start( &client, - AhpHostOptions::default().with_on_exit({ + local_options().with_on_exit({ let calls = calls.clone(); - move |_| { + move |exit| { + gate.lock().unwrap().recv().unwrap(); calls.fetch_add(1, Ordering::SeqCst); - panic!("test callback panic"); + first_tx.send(exit).unwrap(); + // Exercise unwinding without blocking channel closure on panic-hook output. + std::panic::resume_unwind(Box::new("test callback panic")); } }), ); @@ -599,6 +673,15 @@ async fn callback_panic_does_not_break_other_callbacks_or_rpc() { peer.exited(&first.host_id).await; peer.exited(&second.host_id).await; timeout(TIMEOUT, exits.recv()).await.unwrap().unwrap(); + assert_eq!(calls.load(Ordering::SeqCst), 0); + unblock.send(()).unwrap(); + timeout(TIMEOUT, first_exits.recv()).await.unwrap().unwrap(); + assert!( + timeout(TIMEOUT, first_exits.recv()) + .await + .unwrap() + .is_none() + ); assert_eq!(calls.load(Ordering::SeqCst), 1); assert!(client.inner.ahp_host_callbacks.lock().is_empty()); let dispose = tokio::spawn(async move { second.dispose().await }); diff --git a/rust/src/cache_paths.rs b/rust/src/cache_paths.rs index 116e235be1..61c63993c2 100644 --- a/rust/src/cache_paths.rs +++ b/rust/src/cache_paths.rs @@ -43,51 +43,5 @@ fn version_component(version: &str) -> 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 4458396303..df0269b0b8 100644 --- a/rust/src/ffi.rs +++ b/rust/src/ffi.rs @@ -594,151 +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}; +mod tests; - 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); - } -} +#[cfg(test)] +mod shutdown_tests; diff --git a/rust/src/ffi/shutdown_tests.rs b/rust/src/ffi/shutdown_tests.rs new file mode 100644 index 0000000000..712040342f --- /dev/null +++ b/rust/src/ffi/shutdown_tests.rs @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +#![cfg(test)] + +use std::sync::{Mutex, mpsc as std_mpsc}; +use std::time::Duration; + +use super::*; +use crate::Client; + +// The native function pointer cannot capture the per-test shutdown gate. +static SHUTDOWN_GATE: Mutex, std_mpsc::Receiver<()>)>> = + Mutex::new(None); + +unsafe extern "C" fn gated_host_shutdown(_server_id: u32) -> bool { + let gate = SHUTDOWN_GATE.lock().unwrap(); + let (started, release) = gate.as_ref().unwrap(); + started.send(()).unwrap(); + release.recv().unwrap(); + true +} + +unsafe extern "C" fn connection_close(_connection_id: u32) -> bool { + true +} + +unsafe extern "C" fn connection_write( + _connection_id: u32, + _bytes: *const u8, + _length: usize, +) -> bool { + true +} + +#[test] +fn async_stop_keeps_current_thread_executor_responsive_during_native_shutdown() { + let (started_tx, started_rx) = std_mpsc::sync_channel(1); + let (release_tx, release_rx) = std_mpsc::channel(); + let (progress_tx, progress_rx) = std_mpsc::channel(); + let (tick_tx, tick_rx) = tokio::sync::oneshot::channel(); + *SHUTDOWN_GATE.lock().unwrap() = Some((started_tx, release_rx)); + let worker = std::thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + let directory = tempfile::tempdir().unwrap(); + let client = Client::from_streams( + tokio::io::empty(), + tokio::io::sink(), + directory.path().to_path_buf(), + ) + .unwrap(); + *client.inner.ffi_host.lock() = Some(Arc::new(FfiShared { + host_shutdown: gated_host_shutdown, + connection_write, + connection_close, + server_id: AtomicU32::new(11), + connection_id: AtomicU32::new(0), + callback_state: AtomicPtr::new(std::ptr::null_mut()), + closed: AtomicBool::new(false), + operation_lock: parking_lot::Mutex::new(()), + library_path: PathBuf::from("test-runtime"), + })); + client.inner.rpc.force_close(); + let stop = tokio::spawn(async move { client.stop().await }); + tick_rx.await.unwrap(); + progress_tx.send(stop.is_finished()).unwrap(); + stop.await + .unwrap() + .expect_err("the RPC connection is closed") + }) + }); + + let started = started_rx.recv_timeout(Duration::from_secs(5)); + tick_tx.send(()).unwrap(); + let progress = progress_rx.recv_timeout(Duration::from_secs(5)); + release_tx.send(()).unwrap(); + let errors = worker.join().unwrap(); + *SHUTDOWN_GATE.lock().unwrap() = None; + + assert_eq!(started, Ok(()), "native shutdown did not start"); + assert_eq!( + progress, + Ok(false), + "native shutdown blocked the executor or stop completed before cleanup" + ); + assert_eq!(errors.0.len(), 1, "only the closed RPC should fail"); +} 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 c0cd2b9212..2f92c44ecd 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -22,6 +22,12 @@ use crate::types::{RequestId, SessionEvent, SessionId}; /// JSON-RPC method name constants. pub mod rpc_methods { + /// `environments.list` + pub const ENVIRONMENTS_LIST: &str = "environments.list"; + /// `environments.get` + pub const ENVIRONMENTS_GET: &str = "environments.get"; + /// `environments.delete` + pub const ENVIRONMENTS_DELETE: &str = "environments.delete"; /// `host.publishSession` pub const HOST_PUBLISHSESSION: &str = "host.publishSession"; /// `host.createSession` @@ -32,6 +38,8 @@ pub mod rpc_methods { pub const HOST_START: &str = "host.start"; /// `host.dispose` pub const HOST_DISPOSE: &str = "host.dispose"; + /// `host.getEnvironmentCredentials` + pub const HOST_GETENVIRONMENTCREDENTIALS: &str = "host.getEnvironmentCredentials"; /// `host.getConfiguration` pub const HOST_GETCONFIGURATION: &str = "host.getConfiguration"; /// `host.ready` @@ -48,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` @@ -180,6 +198,14 @@ pub mod rpc_methods { pub const MANAGEDSETTINGS_READ: &str = "managedSettings.read"; /// `managedSettings.clearCache` pub const MANAGEDSETTINGS_CLEARCACHE: &str = "managedSettings.clearCache"; + /// `managedSettings.resolve` + pub const MANAGEDSETTINGS_RESOLVE: &str = "managedSettings.resolve"; + /// `managedSettings.schema` + pub const MANAGEDSETTINGS_SCHEMA: &str = "managedSettings.schema"; + /// `managedSettings.validate` + pub const MANAGEDSETTINGS_VALIDATE: &str = "managedSettings.validate"; + /// `managedSettings.compose` + pub const MANAGEDSETTINGS_COMPOSE: &str = "managedSettings.compose"; /// `runtime.shutdown` pub const RUNTIME_SHUTDOWN: &str = "runtime.shutdown"; /// `sessionFs.setProvider` @@ -256,8 +282,17 @@ 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"; + /// `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` @@ -506,6 +541,8 @@ pub mod rpc_methods { pub const SESSION_SKILLS_ENSURELOADED: &str = "session.skills.ensureLoaded"; /// `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` @@ -550,6 +587,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` @@ -577,12 +616,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` @@ -636,6 +681,8 @@ pub mod rpc_methods { pub const SESSION_PROVIDER_ADD: &str = "session.provider.add"; /// `session.provider.sync` pub const SESSION_PROVIDER_SYNC: &str = "session.provider.sync"; + /// `session.provider.withdraw` + pub const SESSION_PROVIDER_WITHDRAW: &str = "session.provider.withdraw"; /// `session.options.update` pub const SESSION_OPTIONS_UPDATE: &str = "session.options.update"; /// `session.lsp.initialize` @@ -693,8 +740,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` @@ -705,6 +756,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"; @@ -917,8 +970,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` @@ -1555,6 +1612,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. /// ///
@@ -2934,6 +3010,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. /// ///
@@ -3027,7 +3143,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. /// ///
/// @@ -3038,13 +3154,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, } @@ -3060,7 +3179,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, } @@ -5608,6 +5727,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. @@ -5626,12 +5751,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 +5962,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. /// ///
@@ -5987,6 +6183,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, @@ -6024,6 +6223,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. /// ///
@@ -6589,6 +6827,249 @@ pub struct DiscoveredMcpServer { pub r#type: 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 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 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, +} + +/// 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 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 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. /// ///
@@ -6607,7 +7088,7 @@ pub struct EnqueueCommandParams { pub display_text: Option, } -/// OneAuth token request supplied by a trusted host application. +/// Hosting capabilities and session capacity advertised by an environment. /// ///
/// @@ -6617,42 +7098,148 @@ 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 EnvironmentCapabilities { + /// Advertised Agent Host Protocol version. #[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 ahp_version: Option, + /// Current session count, when advertised. + #[serde(skip_serializing_if = "Option::is_none")] + pub current_sessions: Option, + /// Feature identifiers advertised by the environment. + pub features: Vec, + /// Maximum session capacity, when advertised. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_sessions: Option, } +/// Identify a user-managed Mission Control environment to delete. +/// +///
+/// +/// **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 EnvironmentsDeleteRequest { + /// User-managed environment to delete. GitHub-managed environments cannot be deleted. + pub environment_id: String, +} + +/// Acknowledgement that the requested environment was deleted. +/// +///
+/// +/// **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 EnvironmentsDeleteResult {} + +/// Identify a Mission Control environment to retrieve. +/// +///
+/// +/// **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 EnvironmentsGetRequest { + /// Identifier assigned by Mission Control. + pub environment_id: String, +} + +/// Safe discovery information. Host-side relay bootstrap credentials are never included. +/// +///
+/// +/// **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 GitHubEnvironment { + /// Hosting capabilities advertised by the environment. #[serde(skip_serializing_if = "Option::is_none")] - pub account_id: Option, - /// Expiry as milliseconds since Unix epoch, when supplied by OneAuth. + pub capabilities: Option, + /// Identifier assigned by Mission Control. + pub id: String, + /// Compute kind reported by Mission Control. + pub kind: EnvironmentKind, + /// Discovery labels attached to the environment. + #[serde(skip_serializing_if = "Option::is_none")] + pub labels: Option>, + /// Timestamp of the last heartbeat received by Mission Control. #[serde(skip_serializing_if = "Option::is_none")] - pub expires_on_timestamp: Option, - /// OneAuth token acquisition outcome discriminator. - pub status: EntraTokenAcquireResultOkStatus, + pub last_heartbeat_at: Option, + /// Human-readable environment name. + pub name: String, + /// Organization identifier, when the environment belongs to an organization. + #[serde(skip_serializing_if = "Option::is_none")] + pub org_id: Option, + /// Identifier of the environment owner. + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_id: Option, + /// Owner category reported by Mission Control. + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_type: Option, + /// Open-ended operational status vocabulary. + pub status: String, } +/// Safe discovery information for the requested environment. +/// +///
+/// +/// **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 EnvironmentsGetResult { + /// The requested environment without relay bootstrap credentials. + pub environment: GitHubEnvironment, +} + +/// Optional discovery filters supported by GitHub Mission Control. +/// +///
+/// +/// **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 EnvironmentsListRequest { + /// Restrict discovery to this compute kind. + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, + /// Operational status, such as online, offline, degraded, waking, or draining. + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, +} + +/// Environments visible to the authenticated caller and matching the supplied filters. +/// +///
+/// +/// **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 EnvironmentsListResult { + /// Safe discovery records without relay bootstrap credentials. + pub environments: Vec, } /// Cursor, batch size, and optional long-poll/filter parameters for reading session events. @@ -7854,7 +8441,7 @@ pub struct HooksDiscoverResult { pub warnings: Vec, } -/// Normalized listener settings delivered only to the supervised hosting participant. +/// GitHub Mission Control registration options. The compute ID is application-owned and stable. /// ///
/// @@ -7864,24 +8451,58 @@ pub struct HooksDiscoverResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct HostConfiguration { +pub struct HostGitHubEnvironmentOptions { + /// Stable application installation identity, reused across host restarts. + pub compute_id: String, + /// Human-readable environment display name. + pub name: String, +} + +/// Normalized local WebSocket listener 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 HostLocalServerConfiguration { /// Hostname or IP address to bind. pub hostname: String, /// Port to bind, with zero requesting OS allocation. pub port: i32, /// Whether the listener requires token authentication. pub require_connection_token: bool, - /// Whether app-owned durable sessions are resumed by the owning application. - #[serde(skip_serializing_if = "Option::is_none")] - pub resume_factory: Option, - /// Whether session materialization is delegated to the owning application. - #[serde(skip_serializing_if = "Option::is_none")] - pub session_factory: Option, /// Secret connection token, absent when authentication is disabled. #[serde(skip_serializing_if = "Option::is_none")] pub token: Option, } +/// Normalized listener settings delivered only to the supervised hosting participant. +/// +///
+/// +/// **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 HostConfiguration { + /// Requested GitHub Mission Control registration. + #[serde(skip_serializing_if = "Option::is_none")] + pub github_environment: Option, + /// Normalized local listener settings, absent for relay-only hosts. + #[serde(skip_serializing_if = "Option::is_none")] + pub local_server: Option, + /// Whether app-owned durable sessions are resumed by the owning application. + pub resume_factory: bool, + /// Whether session materialization is delegated to the owning application. + pub session_factory: bool, +} + /// Stops a connection-owned listener and joins its teardown. /// ///
@@ -7909,6 +8530,27 @@ pub struct HostDisposeRequest { #[serde(rename_all = "camelCase")] pub struct HostEmptyResult {} +/// Private credentials delivered only to a runtime-owned Mission Control hosting participant. +/// +///
+/// +/// **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 HostEnvironmentCredentials { + /// GitHub API base URL for the authenticated service. + pub github_api_url: String, + /// Hostname of the authenticated GitHub service. + pub github_host: String, + /// Mission Control API origin for environment registration and management. + pub mission_control_url: String, + /// Current bearer token for the authenticated GitHub identity. + pub token: String, +} + /// Reports a supervised listener's hosting-task termination and cleanup outcome. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -7925,6 +8567,31 @@ pub struct HostExitedNotification { pub reason: HostExitReason, } +/// Local WebSocket transport 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 HostLocalServerOptions { + /// Listener hostname. Defaults to 127.0.0.1; explicit non-loopback binds are allowed. + #[serde(skip_serializing_if = "Option::is_none")] + pub hostname: Option, + /// Listener port. Omitted or zero requests an OS-allocated port. + #[serde(skip_serializing_if = "Option::is_none")] + pub port: Option, + /// Require token authentication (default true). Cannot be false with a token. + #[serde(skip_serializing_if = "Option::is_none")] + pub require_connection_token: Option, + /// Nonempty connection token. Generated randomly when required and omitted. + #[serde(skip_serializing_if = "Option::is_none")] + pub token: Option, +} + /// Publishes a resident session attached to the listener's owning connection. /// ///
@@ -7971,7 +8638,11 @@ pub struct HostPublishSessionResult { #[serde(rename_all = "camelCase")] pub struct HostReadyRequest { /// Actual bound WebSocket URL. - pub address: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, + /// Registered environment ID, reported only once the relay transport is connected. + #[serde(skip_serializing_if = "Option::is_none")] + pub environment_id: Option, /// Configured secret token, absent when authentication is disabled. #[serde(skip_serializing_if = "Option::is_none")] pub token: Option, @@ -8074,7 +8745,7 @@ pub struct HostSessionReleaseRequest { pub handoff_id: String, } -/// Starts a supervised AHP listener in the runtime's configured working directory. +/// Starts a supervised AHP host with at least one explicitly selected transport. /// ///
/// @@ -8085,26 +8756,20 @@ pub struct HostSessionReleaseRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HostStartRequest { + /// Registers a GitHub Mission Control environment and enables its relay transport. + #[serde(skip_serializing_if = "Option::is_none")] + pub github_environment: Option, /// Caller-generated UUID identifying this connection-owned listener. pub host_id: String, - /// Listener hostname. Defaults to 127.0.0.1; explicit non-loopback binds are allowed. + /// Enables a local WebSocket listener. #[serde(skip_serializing_if = "Option::is_none")] - pub hostname: Option, - /// Listener port. Omitted or zero requests an OS-allocated port. - #[serde(skip_serializing_if = "Option::is_none")] - pub port: Option, - /// Require token authentication (default true). Cannot be false with a token. - #[serde(skip_serializing_if = "Option::is_none")] - pub require_connection_token: Option, + pub local_server: Option, /// Ask the owning application to resume its durable AHP sessions. #[serde(skip_serializing_if = "Option::is_none")] pub resume_factory: Option, /// Ask the owning SDK application to materialize AHP sessions. #[serde(skip_serializing_if = "Option::is_none")] pub session_factory: Option, - /// Nonempty connection token. Generated randomly when required and omitted. - #[serde(skip_serializing_if = "Option::is_none")] - pub token: Option, } /// Listener readiness, returned only after binding and the supervised participant's SDK handshake. @@ -8118,6 +8783,9 @@ pub struct HostStartRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HostStartResult { + /// GitHub Mission Control environment ID, present when its relay transport is ready. + #[serde(skip_serializing_if = "Option::is_none")] + pub environment_id: Option, /// Caller-generated listener UUID. pub host_id: String, /// Separate host process ID, when provided by a legacy runtime. Absent for in-process listeners. @@ -8127,7 +8795,8 @@ pub struct HostStartResult { #[serde(skip_serializing_if = "Option::is_none")] pub token: Option, /// Actual bound WebSocket URL, including the allocated port. - pub url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, } /// Catalogue identity retained from a bound candidate or plan at installation time. @@ -9460,6 +10129,196 @@ pub struct ManagedMcpServerConfig { pub url: String, } +/// Lock state and provenance of one managed setting. +/// +///
+/// +/// **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 ManagedSettingMeta { + /// Whether users and repositories may choose a different value. `false` means policy locks the value. + pub overridable: bool, + /// Channel that supplied this scalar value, matching a `layers[].source`: `device`, `server`, or `policyHelper`. These scalar defaults select one winning channel, not a mixed source. Treat unknown values as additional channels; more may be added. + pub source: String, +} + +/// One candidate channel; absent settings represents a channel that delivered no document. +/// +///
+/// +/// **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 ManagedSettingsComposeLayer { + /// Candidate managed-settings document. Omit when the channel delivered none, as in resolve output. + #[serde(skip_serializing_if = "Option::is_none")] + pub settings: Option, + /// The channel whose candidate document is being supplied. + pub source: ManagedSettingsChannel, +} + +/// Candidate managed-settings documents to merge without applying them. +/// +///
+/// +/// **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 ManagedSettingsComposeRequest { + /// One entry per channel. `source` must be `device`, `server`, or `policyHelper`, each at most once (checked at runtime); order does not matter, because channel precedence is fixed. To preview documents from resolve output, map recognized source strings to ManagedSettingsChannel and copy their settings; generated resolve and compose layer types are distinct. Omitted settings means this channel delivered no document. Supplied documents must be valid within the preview limits; warnings are returned in diagnostics. Compose does not reproduce source-failure state or retained enforcement floors from resolve. + pub layers: Vec, +} + +/// One validation finding for a managed-settings document. +/// +///
+/// +/// **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 ManagedSettingsDiagnostic { + /// Human-readable description of the finding. + pub message: String, + /// Dot-separated path of the offending setting, such as `autoTier.overridable`. Empty for the document as a whole. + pub path: String, + /// Whether the finding rejects the document. + pub severity: ManagedSettingsDiagnosticSeverity, +} + +/// One managed-settings channel and the document it delivered. +/// +///
+/// +/// **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 ManagedSettingsLayer { + /// Validated managed-settings document this channel delivered. Absent when the channel delivered none. + #[serde(skip_serializing_if = "Option::is_none")] + pub settings: Option, + /// Channel identifier: `device` (MDM, plist, registry, or managed file), `server` (account or organization policy), or `policyHelper` (session-local helper output, supported by compose). Treat unknown output values as additional channels; more may be added. + pub source: String, +} + +/// Per-key lock state and provenance for `ManagedSettingsValues`, with the same field names. Producers emit each typed key in values and meta together; both outer objects are omitted when no typed key is set. +/// +///
+/// +/// **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 ManagedSettingsMeta { + /// Lock state and provenance of `values.autoTier`. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, + /// Lock state and provenance of `values.model`. + #[serde(skip_serializing_if = "Option::is_none")] + pub model: Option, +} + +/// Effective enterprise managed settings and contributing channels. Session events report applied policy; sessionless resolve reports an account/device snapshot, and compose reports a non-applying preview of candidate documents. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively. Session-local SDK-client policy is included only in session results. Marked experimental while the managed-settings surface stabilizes. +/// +///
+/// +/// **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 ManagedSettingsResolvedData { + /// Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true. + pub bypass_permissions_disabled: bool, + /// Whether a session-local permissions layer injected by the SDK host was present + #[serde(skip_serializing_if = "Option::is_none")] + pub client_managed: Option, + /// Whether an actual device MDM/plist/registry/file managed-settings layer was present + pub device_managed: bool, + /// Whether managed policy could not be determined (e.g. a failed server fetch) and the session fell back to the fail-closed restriction. When true, restrictions such as disabling bypass-permissions are enforced even though `settings` may be absent. + pub fail_closed: bool, + /// The setting keys under enterprise management in the effective managed settings (e.g. `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force. + pub managed_keys: Vec, + /// Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. + #[serde(skip_serializing_if = "Option::is_none")] + pub permissions_allow_intersected: Option, + /// Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. + #[serde(skip_serializing_if = "Option::is_none")] + pub policy_helper_managed: Option, + /// Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_enabled_by_undetermined_policy: Option, + /// Whether the server (account/org) managed-settings layer was present + pub server_managed: bool, + /// The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. + #[serde(skip_serializing_if = "Option::is_none")] + pub settings: Option, + /// Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. + pub source: ManagedSettingsResolvedSource, +} + +/// Typed effective values of managed settings. Each field mirrors the managed-settings schema key of the same name; more keys are added as they are typed. +/// +///
+/// +/// **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 ManagedSettingsValues { + /// Managed Auto routing preference, used when the selected model is `auto`. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, + /// Managed default model identifier, as configured. New sessions start with it; it can name a model the account cannot use, so hosts match it against the listed models. + #[serde(skip_serializing_if = "Option::is_none")] + pub model: Option, +} + +/// The effective managed settings the runtime would enforce for the given documents, in the same shape `managedSettings.resolve` returns. +/// +///
+/// +/// **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 ManagedSettingsComposeResult { + /// Warnings about ignored content, with paths prefixed by the channel name. + pub diagnostics: Vec, + /// Only the supplied channels, strongest first, with canonical documents. Empty canonical documents are represented as absent settings, as in live resolution. + pub layers: Vec, + /// Per-key lock state and provenance for `values`. + #[serde(skip_serializing_if = "Option::is_none")] + pub meta: Option, + /// Effective managed settings, in the same shape as `session.managedSettings.get`. + pub resolved: ManagedSettingsResolvedData, + /// Typed effective values, as in `managedSettings.resolve`. + #[serde(skip_serializing_if = "Option::is_none")] + pub values: Option, +} + /// Validated device-managed settings discovered before a session exists. /// ///
@@ -9479,7 +10338,7 @@ pub struct ManagedSettingsReadResult { pub settings_json: Option, } -/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +/// Optional opaque account selection or GitHub token whose managed settings are resolved. /// ///
/// @@ -9489,34 +10348,99 @@ pub struct ManagedSettingsReadResult { ///
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ManagedSettingsResolvedData { - /// Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true. - pub bypass_permissions_disabled: bool, - /// Whether a session-local permissions layer injected by the SDK host was present +pub struct ManagedSettingsResolveRequest { + /// Embedding client identity for server policy requests, as in session creation. Omit for the CLI identity. #[serde(skip_serializing_if = "Option::is_none")] - pub client_managed: Option, - /// Whether an actual device MDM/plist/registry/file managed-settings layer was present - pub device_managed: bool, - /// Whether managed policy could not be determined (e.g. a failed server fetch) and the session fell back to the fail-closed restriction. When true, restrictions such as disabling bypass-permissions are enforced even though `settings` may be absent. - pub fail_closed: bool, - /// The setting keys under enterprise management in the effective managed settings (e.g. `model`, `enabledPlugins`, `permissions`). Empty when no managed settings are in force. - pub managed_keys: Vec, - /// Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. + pub client_name: Option, + /// GitHub token to resolve instead of the current account. The call fails when the token cannot be resolved. #[serde(skip_serializing_if = "Option::is_none")] - pub permissions_allow_intersected: Option, - /// Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. + pub git_hub_token: Option, + /// Opaque account identifier returned by `account.getAllUsers`. When omitted, the current account is used, or device policy only when no account is signed in. #[serde(skip_serializing_if = "Option::is_none")] - pub policy_helper_managed: Option, - /// Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. + pub selection_id: Option, +} + +/// Effective enterprise managed settings for an account, resolved without a 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 ManagedSettingsResolveResult { + /// Printable opaque identity of the account the settings were resolved for, suitable for comparison and storage, not an account selectionId. Absent when no account was available, in which case only device policy is reported. #[serde(skip_serializing_if = "Option::is_none")] - pub sandbox_enabled_by_undetermined_policy: Option, - /// Whether the server (account/org) managed-settings layer was present - pub server_managed: bool, - /// The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. + pub account: Option, + /// Warnings about unavailable policy sources or a failed refresh served from cache. A cached response is not proof of a successful live fetch; `resolved.failClosed` separately describes enforcement. + pub diagnostics: Vec, + /// Each managed-settings channel consulted, strongest first, with the validated document it delivered before merging. `resolved.settings` is the merged result. More channels may be added over time. + pub layers: Vec, + /// Per-key lock state and provenance for the entries in `values`, using the same key names. + #[serde(skip_serializing_if = "Option::is_none")] + pub meta: Option, + /// Effective managed settings from the device and account (server) channels, in the same shape as `session.managedSettings.get`, excluding session-local injection. + pub resolved: ManagedSettingsResolvedData, + /// Typed effective values of managed settings, keyed like the managed-settings schema and already resolved across channels, with the `{ "overridable": ... }` wrapper removed. Present when policy sets at least one typed key. Keys not typed here are available in `resolved.settings`. + #[serde(skip_serializing_if = "Option::is_none")] + pub values: Option, +} + +/// The authoring JSON schema for managed settings recognized by this 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 ManagedSettingsSchemaResult { + /// Version of the runtime that owns this schema. + pub runtime_version: String, + /// JSON schema (draft 2020-12) with descriptive shared `x-composition` annotations, not a complete runtime composition contract. Model, effortLevel, and contextTier remain coupled; use `managedSettings.compose` for the runtime's effective result. + pub schema: serde_json::Value, +} + +/// A candidate managed-settings document to validate without applying 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 ManagedSettingsValidateRequest { + /// The document to validate: a JSON object, or a string containing the document's JSON text. Preview documents are limited to 1 MiB and 64 levels of nesting, a stricter resource limit than delivered-policy parsing; violations are returned as diagnostics. + pub content: serde_json::Value, + /// Channel the document is meant for (`device`, `server`, or `policyHelper`). Some keys are only honored in some channels; for example, a `policyHelper` registration is ignored in policy-helper output. When omitted, no channel-specific checks run. + #[serde(skip_serializing_if = "Option::is_none")] + pub layer: Option, +} + +/// Result of validating a managed-settings document. +/// +///
+/// +/// **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 ManagedSettingsValidateResult { + /// Errors that reject the document and warnings about content the runtime ignores. + pub diagnostics: Vec, + /// Canonical form of the document the runtime would apply, with unrecognized keys removed. Absent when the document is invalid. #[serde(skip_serializing_if = "Option::is_none")] pub settings: Option, - /// Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. - pub source: ManagedSettingsResolvedSource, + /// Whether the runtime would accept the document within the preview resource limits. Always equals whether `settings` is present. An invalid document is rejected as a whole. + pub valid: bool, } /// Result of registering a new marketplace. @@ -10127,6 +11051,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. /// ///
@@ -11038,6 +12027,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 { @@ -11092,7 +12098,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. /// ///
/// @@ -11145,6 +12151,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, @@ -11164,6 +12172,7 @@ impl McpOauthLoginOptions { public_client: None, grant_type: None, }, + redirect_uri: None, login_id: None, expected_installation_id: None, } @@ -11211,6 +12220,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 { @@ -11236,7 +12251,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. @@ -11692,6 +12710,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. /// ///
@@ -13246,100 +14460,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. /// ///
@@ -13685,6 +14805,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. /// ///
@@ -13706,6 +15055,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. /// ///
@@ -14060,63 +15565,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. /// ///
@@ -16524,50 +17972,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. /// ///
@@ -16645,6 +18049,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, @@ -16803,6 +18210,41 @@ pub struct ProviderTokenAcquireResult { pub token: String, } +/// Host-managed model selection ids to withdraw from the session's BYOK 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, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct 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. + pub models: Vec, +} + +/// What the withdrawal actually removed from the 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, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderWithdrawResult { + /// 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. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_deselected: Option, + /// Providers removed because one of the withdrawn models was the last entry referencing them. A provider that merely has no models is not removed. + pub providers_removed: Vec, + /// Selection ids that were registered and are now withdrawn. Excludes requested ids that were not present. + pub withdrawn: Vec, +} + /// Blob attachment with inline base64-encoded data /// ///
@@ -18545,7 +19987,7 @@ pub struct SandboxGrantPathForRequestResult { pub success: bool, } -/// Whether this host can run one sandbox policy feature. A session whose effective policy uses an unsupported feature fails each sandboxed command with `reason`. +/// Whether this host can run one sandbox policy feature. A session whose effective policy uses an unsupported feature fails each sandboxed command with `reason`, except `filesystem_enumeration`, whose absence degrades sandboxed PowerShell instead of failing it. /// ///
/// @@ -18556,7 +19998,7 @@ pub struct SandboxGrantPathForRequestResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SandboxHostCapability { - /// The policy feature, as an extensible string: ignore names you do not recognize. Known values: `network` (sandboxed commands can reach the network; on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns), `network_filtering` (host rules and the sandbox proxy; on Linux this needs the same tooling as `network`; on Windows it needs Process Security Environment 1.1 host-loopback support, and a policy that uses it must also set `network.allowLocalNetwork`), `denied_paths` (native enforcement of `filesystem.deniedPaths`), and `shell` (shell commands inside the sandbox; on Windows this needs Process Security Environment 1.1 filesystem enumeration support). + /// The policy feature, as an extensible string: ignore names you do not recognize. Known values: `network` (sandboxed commands can reach the network; on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns), `network_filtering` (host rules and the sandbox proxy; on Linux this needs the same tooling as `network`; on Windows it needs Process Security Environment 1.1 host-loopback support, and a policy that uses it must also set `network.allowLocalNetwork`), `denied_paths` (native enforcement of `filesystem.deniedPaths`), `shell` (shell commands inside the sandbox), and `filesystem_enumeration` (enumerate-only filesystem grants; on Windows this needs Process Security Environment 1.1 filesystem enumeration support, and without it sandboxed PowerShell still runs but cannot resolve its current location; other platforms always report it). pub name: String, /// Human-readable reason and remedy when the feature is unsupported, such as a package to install or an OS update. Present only when `supported` is false. #[serde(skip_serializing_if = "Option::is_none")] @@ -18585,6 +20027,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. /// ///
@@ -19154,35 +20647,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. /// ///
@@ -19449,6 +20913,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. @@ -19593,6 +21060,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. /// ///
@@ -19681,6 +21183,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, @@ -19900,6 +21405,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. /// ///
@@ -22352,7 +23879,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. /// ///
/// @@ -22373,7 +23900,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. /// ///
/// @@ -22384,7 +23911,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, } @@ -27285,6 +28812,27 @@ pub struct HostReleaseSessionResult {} #[serde(rename_all = "camelCase")] pub struct HostDisposeResult {} +/// Private credentials delivered only to a runtime-owned Mission Control hosting participant. +/// +///
+/// +/// **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 HostGetEnvironmentCredentialsResult { + /// GitHub API base URL for the authenticated service. + pub github_api_url: String, + /// Hostname of the authenticated GitHub service. + pub github_host: String, + /// Mission Control API origin for environment registration and management. + pub mission_control_url: String, + /// Current bearer token for the authenticated GitHub identity. + pub token: String, +} + /// Normalized listener settings delivered only to the supervised hosting participant. /// ///
@@ -27296,21 +28844,16 @@ pub struct HostDisposeResult {} #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HostGetConfigurationResult { - /// Hostname or IP address to bind. - pub hostname: String, - /// Port to bind, with zero requesting OS allocation. - pub port: i32, - /// Whether the listener requires token authentication. - pub require_connection_token: bool, - /// Whether app-owned durable sessions are resumed by the owning application. + /// Requested GitHub Mission Control registration. #[serde(skip_serializing_if = "Option::is_none")] - pub resume_factory: Option, - /// Whether session materialization is delegated to the owning application. + pub github_environment: Option, + /// Normalized local listener settings, absent for relay-only hosts. #[serde(skip_serializing_if = "Option::is_none")] - pub session_factory: Option, - /// Secret connection token, absent when authentication is disabled. - #[serde(skip_serializing_if = "Option::is_none")] - pub token: Option, + pub local_server: Option, + /// Whether app-owned durable sessions are resumed by the owning application. + pub resume_factory: bool, + /// Whether session materialization is delegated to the owning application. + pub session_factory: bool, } /// Empty acknowledgement for a completed host lifecycle operation. @@ -27375,6 +28918,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. /// ///
@@ -27952,6 +29575,117 @@ pub struct SessionsGetRemoteControlStatusResult { pub status: serde_json::Value, } +/// 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. /// ///
@@ -28868,6 +30602,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, @@ -30003,23 +31740,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. /// ///
@@ -30557,6 +32277,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. /// ///
@@ -30818,7 +32568,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. @@ -31019,6 +32772,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. /// ///
@@ -31122,6 +32923,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. @@ -31344,7 +33166,7 @@ pub struct SessionManagedSettingsGetParams { pub session_id: SessionId, } -/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +/// Effective enterprise managed settings and contributing channels. Session events report applied policy; sessionless resolve reports an account/device snapshot, and compose reports a non-applying preview of candidate documents. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively. Session-local SDK-client policy is included only in session results. Marked experimental while the managed-settings surface stabilizes. /// ///
/// @@ -31620,6 +33442,26 @@ pub struct SessionProviderSyncResult { pub selection_ids: Vec, } +/// What the withdrawal actually removed from the 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, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionProviderWithdrawResult { + /// 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. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_deselected: Option, + /// Providers removed because one of the withdrawn models was the last entry referencing them. A provider that merely has no models is not removed. + pub providers_removed: Vec, + /// Selection ids that were registered and are now withdrawn. Excludes requested ids that were not present. + pub withdrawn: Vec, +} + /// Indicates whether the session options patch was applied successfully. /// ///
@@ -31986,6 +33828,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. /// ///
@@ -32001,6 +33858,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. /// ///
@@ -32061,6 +33933,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. /// ///
@@ -32907,7 +34794,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. /// ///
/// @@ -32918,7 +34805,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, } @@ -34142,6 +36029,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. /// ///
@@ -34172,7 +36069,7 @@ pub type McpExecuteSamplingResult = HashMap; ///
pub type McpPlanSecretReference = String; -/// Extensible identifier of a sandbox policy feature whose availability varies between hosts. A plain string, so an older client decodes a name added by a newer runtime; ignore names you do not recognize. Known values: `network` — sandboxed commands can reach the network (`network.allowOutbound`, on by default); on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns. `network_filtering` — host rules and the sandbox proxy (`network.allowedHosts`, `network.blockedHosts`, `network.proxy`); on Linux this needs the same tooling as `network`; on Windows it needs a version with Process Security Environment 1.1 host-loopback support, and a policy that uses it must also set `network.allowLocalNetwork`, because Windows reaches the local proxy only together with private-network access. `denied_paths` — native enforcement of `filesystem.deniedPaths`; on Windows this needs a version whose sandbox contract reports denied-path support. `shell` — shell commands inside the sandbox: bash on macOS and Linux, PowerShell on Windows; on Windows this needs a version with Process Security Environment 1.1 filesystem enumeration support. +/// Extensible identifier of a sandbox policy feature whose availability varies between hosts. A plain string, so an older client decodes a name added by a newer runtime; ignore names you do not recognize. Known values: `network` — sandboxed commands can reach the network (`network.allowOutbound`, on by default); on Linux this needs the tooling for Bubblewrap's private network namespace, such as slirp4netns. `network_filtering` — host rules and the sandbox proxy (`network.allowedHosts`, `network.blockedHosts`, `network.proxy`); on Linux this needs the same tooling as `network`; on Windows it needs a version with Process Security Environment 1.1 host-loopback support, and a policy that uses it must also set `network.allowLocalNetwork`, because Windows reaches the local proxy only together with private-network access. `denied_paths` — native enforcement of `filesystem.deniedPaths`; on Windows this needs a version whose sandbox contract reports denied-path support. `shell` — shell commands inside the sandbox: bash on macOS and Linux, PowerShell on Windows. `filesystem_enumeration` — enumerate-only filesystem grants, which PowerShell's drive roots use on Windows; this needs a version with Process Security Environment 1.1 filesystem enumeration support. Without it, sandboxed PowerShell still runs, but `Get-Location` may report the drive root, `Set-Location` may fail, and relative paths may resolve against the drive root; the session also receives a `session.warning` with `warningType` `sandbox`. Other platforms always report it. /// ///
/// @@ -34182,6 +36079,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. /// ///
@@ -34371,6 +36278,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 { @@ -34404,6 +36319,7 @@ pub enum AuthInfo { TokenProvider(TokenProviderAuthInfo), CopilotApiToken(CopilotApiTokenAuthInfo), User(UserAuthInfo), + Account(AccountAuthInfo), GhCli(GhCliAuthInfo), ApiKey(ApiKeyAuthInfo), } @@ -35110,6 +37026,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, @@ -35171,7 +37090,7 @@ pub enum AuthLoginStepCompletedKind { Completed, } -/// Terminal disposition of a login persistence attempt. +/// Disposition of a login attempt, including pending user decisions. /// ///
/// @@ -35181,12 +37100,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, @@ -36935,6 +38857,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. /// ///
@@ -37404,7 +39444,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. /// ///
/// @@ -37413,39 +39453,54 @@ 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. +/// Typed outcome for a provider 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, PartialEq, Eq, Serialize, Deserialize)] -pub enum EntraTokenAcquireResultInteractionRequiredStatus { - #[serde(rename = "interaction-required")] +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] - InteractionRequired, + #[serde(other)] + Unknown, } -/// Result of a OneAuth token acquisition. +/// GitHub Mission Control compute kind. /// ///
/// @@ -37453,11 +39508,27 @@ 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 EnvironmentKind { + /// A user-managed environment on a local machine. + #[serde(rename = "user-local")] + UserLocal, + /// A user-managed environment in a GitHub Codespace. + #[serde(rename = "user-codespace")] + UserCodespace, + /// A GitHub-managed environment backed by GitHub Actions. + #[serde(rename = "managed-actions")] + ManagedActions, + /// A GitHub-managed sandbox environment. + #[serde(rename = "managed-sandbox")] + ManagedSandbox, + /// A GitHub-managed cloud coding agent environment. + #[serde(rename = "managed-cca")] + ManagedCca, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, } /// 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. @@ -38593,6 +40664,53 @@ pub enum SessionLogLevel { Unknown, } +/// A channel accepted by managedSettings.compose. +/// +///
+/// +/// **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 ManagedSettingsChannel { + /// Device policy, the strongest channel. + #[serde(rename = "device")] + Device, + /// Account or organization policy. + #[serde(rename = "server")] + Server, + /// Session-local helper output, the weakest channel. + #[serde(rename = "policyHelper")] + PolicyHelper, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Severity of a managed-settings validation finding. +/// +///
+/// +/// **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 ManagedSettingsDiagnosticSeverity { + /// The runtime rejects the document. + #[serde(rename = "error")] + Error, + /// The runtime accepts the document but ignores the flagged content. + #[serde(rename = "warning")] + Warning, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Allowed values for the `McpAppsHostContextDetailsAvailableDisplayMode` enumeration. /// ///
@@ -39542,6 +41660,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. /// ///
@@ -39864,7 +42004,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. /// ///
/// @@ -39873,13 +42013,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)] @@ -39955,6 +42188,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. /// ///
@@ -40899,75 +43223,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 { @@ -41321,6 +43576,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 /// ///
@@ -42018,6 +44301,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 11acb1fb1e..d14723e670 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> { } } + /// `environments.*` sub-namespace. + pub fn environments(&self) -> ClientRpcEnvironments<'a> { + ClientRpcEnvironments { + client: self.client, + } + } + /// `extensions.*` sub-namespace. pub fn extensions(&self) -> ClientRpcExtensions<'a> { ClientRpcExtensions { @@ -430,45 +430,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> { @@ -707,6 +668,107 @@ impl<'a> ClientRpcCommands<'a> { } } +/// `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> { @@ -926,13 +988,13 @@ impl<'a> ClientRpcHost<'a> { Ok(serde_json::from_value(_value)?) } - /// Starts a connection-owned local AHP listener as a supervised SDK participant. + /// Starts a connection-owned AHP host with explicit localServer and/or githubEnvironment transports as a supervised SDK participant. /// /// Wire method: `host.start`. /// /// # Parameters /// - /// * `params` - Starts a supervised AHP listener in the runtime's configured working directory. + /// * `params` - Starts a supervised AHP host with at least one explicitly selected transport. /// /// # Returns /// @@ -982,6 +1044,35 @@ impl<'a> ClientRpcHost<'a> { Ok(serde_json::from_value(_value)?) } + /// Resolves current authenticated credentials and remote-control policy only for the runtime-owned Mission Control hosting participant. + /// + /// Wire method: `host.getEnvironmentCredentials`. + /// + /// # Returns + /// + /// Private credentials delivered only to a runtime-owned Mission Control hosting participant. + /// + ///
+ /// + /// **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_environment_credentials( + &self, + ) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call( + rpc_methods::HOST_GETENVIRONMENTCREDENTIALS, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Returns listener settings only to the supervised hosting participant over its SDK connection. /// /// Wire method: `host.getConfiguration`. @@ -1215,7 +1306,7 @@ pub struct ClientRpcManagedSettings<'a> { } impl<'a> ClientRpcManagedSettings<'a> { - /// Discovers device-managed settings from production MDM and managed-file sources, validates them against the runtime-owned managed-settings schema, and returns the canonical JSON without requiring a session. + /// Discovers device-managed settings from production MDM and managed-file sources, validates them against the runtime-owned managed-settings schema, and returns the canonical JSON without requiring a session. `managedSettings.resolve` returns the same device settings together with the account's server policy. /// /// Wire method: `managedSettings.read`. /// @@ -1239,7 +1330,7 @@ impl<'a> ClientRpcManagedSettings<'a> { Ok(serde_json::from_value(_value)?) } - /// Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing "sync account policy" should start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed. + /// Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing "sync account policy" should call `managedSettings.resolve` or start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed. /// /// Wire method: `managedSettings.clearCache`. /// @@ -1258,6 +1349,147 @@ impl<'a> ClientRpcManagedSettings<'a> { .await?; Ok(()) } + + /// Resolves the effective enterprise managed settings without a session, from the device channel and, when an account is available, the account's server policy through the same per-account cache sessions use. A cached server policy less than an hour old is used without a fetch; otherwise the policy is fetched, and when the fetch fails a cached policy up to 24 hours old is used instead, unless `forceRemoteSettingsRefresh` requires a live fetch. With no account requested or signed in, it reports device policy only; signing out removes the account's cached policy. It can fetch server policy over the network when the cache is stale, so call it off latency-critical paths such as startup rather than before listing models. The policy helper is not run. `layers` lists each channel's document before merging, and `values` and `meta` carry typed effective values and their lock state for the keys typed so far. + /// + /// Wire method: `managedSettings.resolve`. + /// + /// # Returns + /// + /// Effective enterprise managed settings for an account, resolved without a 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 resolve(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::MANAGEDSETTINGS_RESOLVE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Resolves the effective enterprise managed settings without a session, from the device channel and, when an account is available, the account's server policy through the same per-account cache sessions use. A cached server policy less than an hour old is used without a fetch; otherwise the policy is fetched, and when the fetch fails a cached policy up to 24 hours old is used instead, unless `forceRemoteSettingsRefresh` requires a live fetch. With no account requested or signed in, it reports device policy only; signing out removes the account's cached policy. It can fetch server policy over the network when the cache is stale, so call it off latency-critical paths such as startup rather than before listing models. The policy helper is not run. `layers` lists each channel's document before merging, and `values` and `meta` carry typed effective values and their lock state for the keys typed so far. + /// + /// Wire method: `managedSettings.resolve`. + /// + /// # Parameters + /// + /// * `params` - Optional opaque account selection or GitHub token whose managed settings are resolved. + /// + /// # Returns + /// + /// Effective enterprise managed settings for an account, resolved without a 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 resolve_with_params( + &self, + params: ManagedSettingsResolveRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::MANAGEDSETTINGS_RESOLVE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Returns the managed-settings authoring JSON schema with descriptive `x-composition` annotations aligned with the shared settings-engine vocabulary. These annotations are not a complete runtime composition contract: model, effortLevel, and contextTier remain coupled. Use `managedSettings.compose` for the runtime's effective result. Performs no I/O. + /// + /// Wire method: `managedSettings.schema`. + /// + /// # Returns + /// + /// The authoring JSON schema for managed settings recognized by this runtime. + /// + ///
+ /// + /// **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 schema(&self) -> Result { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::MANAGEDSETTINGS_SCHEMA, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Validates a candidate managed-settings document the way the runtime validates delivered policy, without applying it. Reports errors that would reject the document, warnings for content the runtime ignores, and the canonical document it would apply. Document text nested more than 64 levels deep is rejected. Performs no I/O. + /// + /// Wire method: `managedSettings.validate`. + /// + /// # Parameters + /// + /// * `params` - A candidate managed-settings document to validate without applying it. + /// + /// # Returns + /// + /// Result of validating a managed-settings document. + /// + ///
+ /// + /// **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 validate( + &self, + params: ManagedSettingsValidateRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::MANAGEDSETTINGS_VALIDATE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Merges candidate managed-settings documents for the device, server, and policy-helper channels into the effective settings the runtime would enforce on this host, using the same precedence and composition rules as live resolution, without applying them. Like live resolution, a server's advisory sandbox force-enable is declined on a host that cannot run the sandbox. Does not fetch policy or read policy files, but may perform blocking OS or subprocess probes for sandbox support. Preview documents are limited to 1 MiB and 64 levels of nesting. + /// + /// Wire method: `managedSettings.compose`. + /// + /// # Parameters + /// + /// * `params` - Candidate managed-settings documents to merge without applying them. + /// + /// # Returns + /// + /// The effective managed settings the runtime would enforce for the given documents, in the same shape `managedSettings.resolve` returns. + /// + ///
+ /// + /// **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 compose( + &self, + params: ManagedSettingsComposeRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::MANAGEDSETTINGS_COMPOSE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } } /// `mcp.*` RPCs. @@ -2334,6 +2566,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`. @@ -2359,24 +2598,24 @@ impl<'a> ClientRpcSandbox<'a> { } } -/// `secrets.*` RPCs. +/// `sandbox.proxyCa.*` RPCs. #[derive(Clone, Copy)] -pub struct ClientRpcSecrets<'a> { +pub struct ClientRpcSandboxProxyCa<'a> { pub(crate) client: &'a Client, } -impl<'a> ClientRpcSecrets<'a> { - /// Registers secret values for redaction in session logs and exports. The SDK calls this to inject dynamically generated secret values (e.g., OIDC tokens). +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: `secrets.addFilterValues`. + /// Wire method: `sandbox.proxyCa.getStatus`. /// /// # Parameters /// - /// * `params` - Secret values to add to the redaction filter. + /// * `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 /// - /// Confirmation that the secret values were registered. + /// Status of the persistent certificate authority of the sandbox credential proxy. /// ///
/// @@ -2385,10 +2624,166 @@ impl<'a> ClientRpcSecrets<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn add_filter_values( + pub async fn get_status( &self, - params: SecretsAddFilterValuesRequest, - ) -> Result { + 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> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcSecrets<'a> { + /// Registers secret values for redaction in session logs and exports. The SDK calls this to inject dynamically generated secret values (e.g., OIDC tokens). + /// + /// Wire method: `secrets.addFilterValues`. + /// + /// # Parameters + /// + /// * `params` - Secret values to add to the redaction filter. + /// + /// # Returns + /// + /// Confirmation that the secret values were registered. + /// + ///
+ /// + /// **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 add_filter_values( + &self, + params: SecretsAddFilterValuesRequest, + ) -> Result { let wire_params = serde_json::to_value(params)?; let _value = self .client @@ -4258,6 +4653,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 { @@ -5683,6 +6085,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`. @@ -5945,6 +6375,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`. @@ -6061,13 +6526,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. /// ///
/// @@ -6076,7 +6541,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 @@ -7174,10 +7639,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 @@ -7185,14 +7654,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)?) } } @@ -7315,7 +7784,7 @@ impl<'a> SessionRpcManagedSettings<'a> { /// /// # Returns /// - /// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. + /// Effective enterprise managed settings and contributing channels. Session events report applied policy; sessionless resolve reports an account/device snapshot, and compose reports a non-applying preview of candidate documents. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively. Session-local SDK-client policy is included only in session results. Marked experimental while the managed-settings surface stabilizes. /// ///
/// @@ -7363,6 +7832,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 { @@ -7370,7 +7846,7 @@ 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. + /// Lists materialized MCP servers and their connection status. Cache misses may start and wait for MCP servers. /// /// Wire method: `session.mcp.list`. /// @@ -7395,6 +7871,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`. @@ -8413,7 +8914,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 /// @@ -8443,7 +8944,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 /// @@ -8472,6 +8973,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`. @@ -8607,6 +9134,74 @@ impl<'a> SessionRpcMcpOauth<'a> { } } +/// `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. + /// + ///
+ /// + /// **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: 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_PROMPTS_LIST, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// 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.prompts.get`. + /// + /// # Parameters + /// + /// * `params` - MCP server, prompt name, and optional string-valued arguments. + /// + /// # Returns + /// + /// Prompt messages returned by the MCP server without sending them to the model. + /// + ///
+ /// + /// **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: 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_PROMPTS_GET, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `session.mcp.resources.*` RPCs. #[derive(Clone, Copy)] pub struct SessionRpcMcpResources<'a> { @@ -10709,30 +11304,235 @@ impl<'a> SessionRpcPluginsMarketplaces<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn list(&self) -> Result { + pub async fn list(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PLUGINS_MARKETPLACES_LIST, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Adds a marketplace when permitted by the live session's retained managed policy. + /// + /// Wire method: `session.plugins.marketplaces.add`. + /// + /// # Parameters + /// + /// * `params` - Marketplace source and optional working directory for relative-path resolution. + /// + /// # Returns + /// + /// Result of registering a new marketplace. + /// + ///
+ /// + /// **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 add( + &self, + params: PluginsMarketplacesAddRequest, + ) -> 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_PLUGINS_MARKETPLACES_ADD, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Removes a marketplace when permitted by the live session's retained managed policy. + /// + /// Wire method: `session.plugins.marketplaces.remove`. + /// + /// # Parameters + /// + /// * `params` - Name of the marketplace to remove and an optional force flag. + /// + /// # Returns + /// + /// Outcome of the remove attempt, including dependent-plugin info when applicable. + /// + ///
+ /// + /// **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, + params: PluginsMarketplacesRemoveRequest, + ) -> 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_PLUGINS_MARKETPLACES_REMOVE, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Browses a marketplace resolved through the live session's working directory and retained managed policy. + /// + /// Wire method: `session.plugins.marketplaces.browse`. + /// + /// # Parameters + /// + /// * `params` - Name of the marketplace whose plugin catalog to fetch. + /// + /// # Returns + /// + /// Plugins advertised by the marketplace. + /// + ///
+ /// + /// **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 browse( + &self, + params: PluginsMarketplacesBrowseRequest, + ) -> 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_PLUGINS_MARKETPLACES_BROWSE, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Refreshes marketplaces resolved through the live session's working directory and retained managed policy. + /// + /// Wire method: `session.plugins.marketplaces.refresh`. + /// + /// # Returns + /// + /// Result of refreshing one or more marketplace catalogs. + /// + ///
+ /// + /// **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) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_PLUGINS_MARKETPLACES_REFRESH, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Refreshes marketplaces resolved through the live session's working directory and retained managed policy. + /// + /// Wire method: `session.plugins.marketplaces.refresh`. + /// + /// # Parameters + /// + /// * `params` - Optional marketplace name; omit to refresh all. + /// + /// # Returns + /// + /// Result of refreshing one or more marketplace catalogs. + /// + ///
+ /// + /// **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_with_params( + &self, + params: PluginsMarketplacesRefreshRequest, + ) -> 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_PLUGINS_MARKETPLACES_REFRESH, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + +/// `session.provider.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcProvider<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcProvider<'a> { + /// Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses. + /// + /// Wire method: `session.provider.getEndpoint`. + /// + /// # Returns + /// + /// A snapshot of the provider endpoint the session is currently configured to talk to. + /// + ///
+ /// + /// **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_endpoint(&self) -> Result { let wire_params = serde_json::json!({ "sessionId": self.session.id() }); let _value = self .session .client() - .call( - rpc_methods::SESSION_PLUGINS_MARKETPLACES_LIST, - Some(wire_params), - ) + .call(rpc_methods::SESSION_PROVIDER_GETENDPOINT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Adds a marketplace when permitted by the live session's retained managed policy. + /// Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses. /// - /// Wire method: `session.plugins.marketplaces.add`. + /// Wire method: `session.provider.getEndpoint`. /// /// # Parameters /// - /// * `params` - Marketplace source and optional working directory for relative-path resolution. + /// * `params` - Optional model identifier to scope the endpoint snapshot to. /// /// # Returns /// - /// Result of registering a new marketplace. + /// A snapshot of the provider endpoint the session is currently configured to talk to. /// ///
/// @@ -10741,34 +11541,31 @@ impl<'a> SessionRpcPluginsMarketplaces<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn add( + pub async fn get_endpoint_with_params( &self, - params: PluginsMarketplacesAddRequest, - ) -> Result { + params: ProviderGetEndpointRequest, + ) -> 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_PLUGINS_MARKETPLACES_ADD, - Some(wire_params), - ) + .call(rpc_methods::SESSION_PROVIDER_GETENDPOINT, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Removes a marketplace when permitted by the live session's retained managed policy. + /// Adds BYOK providers and/or models to the session's registry at runtime, extending the additive registry built from the session's `providers`/`models` options. Both fields are optional, so a call may add providers only, models only, or both. Within a single call providers are registered before models, so a model may reference a provider added in the same call; across calls a model may reference any provider already registered (from session creation or a prior add). A model whose referenced provider is not registered by the end of the call is rejected. Newly added models become selectable via `model.list` / `model.switchTo` and are inherited by sub-agents spawned afterwards. /// - /// Wire method: `session.plugins.marketplaces.remove`. + /// Wire method: `session.provider.add`. /// /// # Parameters /// - /// * `params` - Name of the marketplace to remove and an optional force flag. + /// * `params` - BYOK providers and/or models to add to the session's registry at runtime. Both fields are optional; provide providers, models, or both. /// /// # Returns /// - /// Outcome of the remove attempt, including dependent-plugin info when applicable. + /// The selectable model entries synthesized for the models added by this call. /// ///
/// @@ -10777,34 +11574,28 @@ impl<'a> SessionRpcPluginsMarketplaces<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn remove( - &self, - params: PluginsMarketplacesRemoveRequest, - ) -> Result { + pub async fn add(&self, params: ProviderAddRequest) -> 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_PLUGINS_MARKETPLACES_REMOVE, - Some(wire_params), - ) + .call(rpc_methods::SESSION_PROVIDER_ADD, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Browses a marketplace resolved through the live session's working directory and retained managed policy. + /// Atomically updates the session's BYOK provider and model registry by applying the supplied snapshot, replacing existing entries, updating models, or removing entries absent from the snapshot. /// - /// Wire method: `session.plugins.marketplaces.browse`. + /// Wire method: `session.provider.sync`. /// /// # Parameters /// - /// * `params` - Name of the marketplace whose plugin catalog to fetch. + /// * `params` - Authoritative BYOK provider and model registry snapshot to apply atomically to the session. /// /// # Returns /// - /// Plugins advertised by the marketplace. + /// The selectable model entries and selection ids synthesized for the synchronized BYOK models. /// ///
/// @@ -10813,30 +11604,28 @@ impl<'a> SessionRpcPluginsMarketplaces<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn browse( - &self, - params: PluginsMarketplacesBrowseRequest, - ) -> Result { + pub async fn sync(&self, params: ProviderSyncRequest) -> 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_PLUGINS_MARKETPLACES_BROWSE, - Some(wire_params), - ) + .call(rpc_methods::SESSION_PROVIDER_SYNC, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Refreshes marketplaces resolved through the live session's working directory and retained managed policy. + /// Withdraws named host-managed models from the session's BYOK registry, leaving every other entry untouched. The scoped counterpart to `provider.sync`: a snapshot can only describe entries the caller knows about, so using it to remove one model silently withdraws rows registered by another source, such as a plugin calling `provider.add` at runtime. Naming what to remove leaves unrelated entries alone. Selection ids that are not registered are ignored, so withdrawal is idempotent. A provider is removed only when one of the withdrawn models was the last entry referencing it; a provider that simply has no models, which is the normal state while its rows are supplied by catalog discovery, is left in place. /// - /// Wire method: `session.plugins.marketplaces.refresh`. + /// Wire method: `session.provider.withdraw`. + /// + /// # Parameters + /// + /// * `params` - Host-managed model selection ids to withdraw from the session's BYOK registry. /// /// # Returns /// - /// Result of refreshing one or more marketplace catalogs. + /// What the withdrawal actually removed from the registry. /// ///
/// @@ -10845,30 +11634,42 @@ impl<'a> SessionRpcPluginsMarketplaces<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn refresh(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + pub async fn withdraw( + &self, + params: ProviderWithdrawRequest, + ) -> 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_PLUGINS_MARKETPLACES_REFRESH, - Some(wire_params), - ) + .call(rpc_methods::SESSION_PROVIDER_WITHDRAW, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Refreshes marketplaces resolved through the live session's working directory and retained managed policy. - /// - /// Wire method: `session.plugins.marketplaces.refresh`. - /// - /// # Parameters +/// `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. /// - /// * `params` - Optional marketplace name; omit to refresh all. + /// Wire method: `session.providers.getCatalog`. /// /// # Returns /// - /// Result of refreshing one or more marketplace catalogs. + /// Normalized model-provider adapter definitions available to the session, not discovered instances. /// ///
/// @@ -10877,38 +11678,27 @@ impl<'a> SessionRpcPluginsMarketplaces<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn refresh_with_params( - &self, - params: PluginsMarketplacesRefreshRequest, - ) -> Result { - let mut wire_params = serde_json::to_value(params)?; - wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + 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_PLUGINS_MARKETPLACES_REFRESH, - Some(wire_params), - ) + .call(rpc_methods::SESSION_PROVIDERS_GETCATALOG, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } -} - -/// `session.provider.*` RPCs. -#[derive(Clone, Copy)] -pub struct SessionRpcProvider<'a> { - pub(crate) session: &'a Session, -} -impl<'a> SessionRpcProvider<'a> { - /// Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses. + /// Discovers reachable instances using an adapter from this session's effective provider catalog and provider-specific discovery input. /// - /// Wire method: `session.provider.getEndpoint`. + /// Wire method: `session.providers.discover`. + /// + /// # Parameters + /// + /// * `params` - Provider discovery parameters. /// /// # Returns /// - /// A snapshot of the provider endpoint the session is currently configured to talk to. + /// Provider instances found by a discovery operation. /// ///
/// @@ -10917,27 +11707,31 @@ impl<'a> SessionRpcProvider<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_endpoint(&self) -> Result { - let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + 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_PROVIDER_GETENDPOINT, Some(wire_params)) + .call(rpc_methods::SESSION_PROVIDERS_DISCOVER, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } - /// Returns the provider endpoint and credentials the session is currently configured to talk to, so the caller can make inference calls directly against the same backend the session uses. + /// Gets current health and version information for a discovered model-provider instance. /// - /// Wire method: `session.provider.getEndpoint`. + /// Wire method: `session.providers.getStatus`. /// /// # Parameters /// - /// * `params` - Optional model identifier to scope the endpoint snapshot to. + /// * `params` - Provider status request parameters. /// /// # Returns /// - /// A snapshot of the provider endpoint the session is currently configured to talk to. + /// Current health information for a provider instance. /// ///
/// @@ -10946,31 +11740,39 @@ impl<'a> SessionRpcProvider<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn get_endpoint_with_params( + pub async fn get_status( &self, - params: ProviderGetEndpointRequest, - ) -> Result { + 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_PROVIDER_GETENDPOINT, Some(wire_params)) + .call(rpc_methods::SESSION_PROVIDERS_GETSTATUS, Some(wire_params)) .await?; Ok(serde_json::from_value(_value)?) } +} - /// Adds BYOK providers and/or models to the session's registry at runtime, extending the additive registry built from the session's `providers`/`models` options. Both fields are optional, so a call may add providers only, models only, or both. Within a single call providers are registered before models, so a model may reference a provider added in the same call; across calls a model may reference any provider already registered (from session creation or a prior add). A model whose referenced provider is not registered by the end of the call is rejected. Newly added models become selectable via `model.list` / `model.switchTo` and are inherited by sub-agents spawned afterwards. +/// `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.provider.add`. + /// Wire method: `session.providers.models.list`. /// /// # Parameters /// - /// * `params` - BYOK providers and/or models to add to the session's registry at runtime. Both fields are optional; provide providers, models, or both. + /// * `params` - Provider model inventory request parameters. /// /// # Returns /// - /// The selectable model entries synthesized for the models added by this call. + /// 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. /// ///
/// @@ -10979,28 +11781,34 @@ impl<'a> SessionRpcProvider<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn add(&self, params: ProviderAddRequest) -> Result { + 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_PROVIDER_ADD, Some(wire_params)) + .call( + rpc_methods::SESSION_PROVIDERS_MODELS_LIST, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } - /// Atomically updates the session's BYOK provider and model registry by applying the supplied snapshot, replacing existing entries, updating models, or removing entries absent from the snapshot. + /// 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.provider.sync`. + /// Wire method: `session.providers.models.prepareConfiguration`. /// /// # Parameters /// - /// * `params` - Authoritative BYOK provider and model registry snapshot to apply atomically to the session. + /// * `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 /// - /// The selectable model entries and selection ids synthesized for the synchronized BYOK models. + /// 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. /// ///
/// @@ -11009,13 +11817,19 @@ impl<'a> SessionRpcProvider<'a> { /// SDK and CLI versions if your code depends on it. /// ///
- pub async fn sync(&self, params: ProviderSyncRequest) -> Result { + 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_PROVIDER_SYNC, Some(wire_params)) + .call( + rpc_methods::SESSION_PROVIDERS_MODELS_PREPARECONFIGURATION, + Some(wire_params), + ) .await?; Ok(serde_json::from_value(_value)?) } @@ -12193,17 +13007,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. /// ///
/// @@ -13326,6 +14140,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`. @@ -13362,6 +14212,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`. @@ -13506,6 +14392,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 0d97dc9c4b..1e9dff8586 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -216,6 +216,8 @@ pub enum SessionEventType { PromptCacheBreak, #[serde(rename = "model.call_failure")] ModelCallFailure, + #[serde(rename = "model.call_final_result")] + ModelCallFinalResult, #[serde(rename = "model.call_finished")] ModelCallFinished, #[serde(rename = "model.call_start")] @@ -226,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")] @@ -438,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")] @@ -764,6 +772,8 @@ pub enum SessionEventData { PromptCacheBreak(PromptCacheBreakData), #[serde(rename = "model.call_failure")] ModelCallFailure(ModelCallFailureData), + #[serde(rename = "model.call_final_result")] + ModelCallFinalResult(ModelCallFinalResultData), #[serde(rename = "model.call_finished")] ModelCallFinished(ModelCallFinishedData), #[serde(rename = "model.call_start")] @@ -774,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")] @@ -958,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")] @@ -1184,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, @@ -1234,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, @@ -1530,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, @@ -3226,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, } @@ -3534,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")] @@ -3613,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")] @@ -3745,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")] @@ -3797,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, @@ -3843,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, @@ -3875,6 +3919,19 @@ pub struct ModelCallFailureData { pub transport: Option, } +/// Session event "model.call_final_result". Internal telemetry result for one logical model operation after all orchestrator-owned retries settle +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelCallFinalResultData { + /// Whether the final attempt used a bring-your-own-key provider + #[serde(skip_serializing_if = "Option::is_none")] + pub is_byok: Option, + /// Model identifier used by the final attempt + pub model: String, + /// Bounded result of the final attempt + pub result: ModelCallFinalResult, +} + /// Session event "model.call_finished". Final lifecycle outcome for one logical model dispatch. A logical dispatch may include internal reconnect or fallback work, so event count is not provider HTTP-request count. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -4063,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, } @@ -4097,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")] @@ -4560,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. /// ///
@@ -7393,12 +7504,15 @@ 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, } -/// Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +/// Session event "session.managed_settings_resolved". Effective enterprise managed settings applied to the session and their contributing channels. Emitted whenever managed policy is applied or reapplied, including session start, resume, and account switch. This ephemeral live snapshot is delivered to subscribers but not persisted to the session event log; initial resolution occurs before session.start. /// ///
/// @@ -7544,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")] @@ -7747,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 @@ -9275,6 +9462,39 @@ pub enum ModelCallFailureSource { Unknown, } +/// Final bounded result of one logical model operation after its internal retry loop settles +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ModelCallFinalResult { + /// The final attempt succeeded. + #[serde(rename = "success")] + Success, + /// The final attempt failed with HTTP 400. + #[serde(rename = "http_400")] + Http400, + /// The final attempt failed with HTTP 413. + #[serde(rename = "http_413")] + Http413, + /// The final attempt failed with HTTP 429. + #[serde(rename = "http_429")] + Http429, + /// The final attempt failed with another HTTP 4xx status. + #[serde(rename = "http_4xx")] + Http4xx, + /// The final attempt failed with an HTTP 5xx status. + #[serde(rename = "http_5xx")] + Http5xx, + /// The final attempt failed in the request transport. + #[serde(rename = "transport_error")] + TransportError, + /// The final attempt failed without another bounded classification. + #[serde(rename = "other_error")] + OtherError, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Final outcome of one logical model dispatch after response acceptance processing #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum ModelCallFinishedOutcome { @@ -9377,6 +9597,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 { @@ -11128,6 +11412,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 { @@ -11218,7 +11553,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..42a52092ba 100644 --- a/rust/src/installation_confirmation.rs +++ b/rust/src/installation_confirmation.rs @@ -140,6 +140,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; } diff --git a/rust/src/jsonrpc.rs b/rust/src/jsonrpc.rs index 32cd242b77..8da2c72fb5 100644 --- a/rust/src/jsonrpc.rs +++ b/rust/src/jsonrpc.rs @@ -889,161 +889,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 5c59b8c53b..43040d0944 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. @@ -1262,6 +1269,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 +1444,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 +1519,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 +1555,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 +1581,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 +1651,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 +1707,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 +1831,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 +1893,7 @@ impl Client { Some(provider), false, false, + false, None, None, None, @@ -1893,6 +1944,7 @@ impl Client { None, false, false, + false, Some(provider), None, None, @@ -1922,6 +1974,7 @@ impl Client { None, false, false, + false, None, None, token, @@ -1951,6 +2004,7 @@ impl Client { None, false, false, + false, None, Some(on_github_telemetry), None, @@ -1991,6 +2045,7 @@ impl Client { None, false, false, + false, None, None, None, @@ -2013,6 +2068,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 +2124,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 +2524,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). @@ -3105,8 +3167,11 @@ impl Client { self.inner.rpc.force_close(); #[cfg(feature = "in-process")] { - if let Some(host) = self.inner.ffi_host.lock().take() { - host.close(); + let host = self.inner.ffi_host.lock().take(); + if let Some(host) = host + && let Err(error) = tokio::task::spawn_blocking(move || host.close()).await + { + errors.push(Error::new(ErrorKind::Io, error)); } } @@ -3242,814 +3307,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..02fd5cfde2 100644 --- a/rust/src/router.rs +++ b/rust/src/router.rs @@ -327,35 +327,4 @@ 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)); - } -} +mod tests; diff --git a/rust/src/router/tests.rs b/rust/src/router/tests.rs new file mode 100644 index 0000000000..4d89fd937f --- /dev/null +++ b/rust/src/router/tests.rs @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * 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)); +} diff --git a/rust/src/session.rs b/rust/src/session.rs index 193203c3cb..14864a3137 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -37,12 +37,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 +92,9 @@ pub(crate) struct SessionHandlers { pub user_input: Option>, pub exit_plan_mode: Option>, pub auto_mode_switch: Option>, - pub tools: Arc>>, + /// Shared with the owning [`Session`], which replaces the map when the + /// runtime accepts a [`Session::set_tools`] call. + pub tools: Arc>, } type PendingExternalTools = Arc>>>; @@ -490,6 +493,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 +521,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 +605,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 +1142,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 @@ -1627,6 +1752,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 +1766,7 @@ 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)), + tools: tool_handlers.clone(), }; let hooks = runtime.hooks_handler.take(); let transforms = runtime.system_message_transform.take(); @@ -1670,6 +1798,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 +1970,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 +1979,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 +2017,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 +2035,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 +2097,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 +2111,7 @@ 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)), + tools: tool_handlers.clone(), }; let hooks = runtime.hooks_handler.take(); let transforms = runtime.system_message_transform.take(); @@ -2000,6 +2143,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; @@ -2133,6 +2285,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 +2294,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, @@ -2295,16 +2450,15 @@ impl PreparedSession { event_tx, shutdown, } = self; + // The startup state machines exceed 16 KiB; boxing them keeps this + // future, and every public caller awaiting it, small enough for + // 2 MiB worker stacks and `clippy::large_futures`. match kind { PreparedKind::Create(config) => { - client - .start_prepared_create(*config, event_tx, shutdown) - .await + Box::pin(client.start_prepared_create(*config, event_tx, shutdown)).await } PreparedKind::Resume(config) => { - client - .start_prepared_resume(*config, event_tx, shutdown) - .await + Box::pin(client.start_prepared_resume(*config, event_tx, shutdown)).await } } } @@ -2928,7 +3082,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; @@ -3690,311 +3844,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/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/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..dbf40ce4f5 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -43,9 +43,9 @@ 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, }; pub use crate::trace_context::{TraceContext, TraceContextProvider}; @@ -532,6 +532,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 +1268,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 +1327,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 +1375,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 +1541,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 +1576,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 +1614,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()); @@ -2585,7 +2638,7 @@ 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 bearer_token_providers: HashMap>, @@ -2623,19 +2676,7 @@ impl SessionConfig { let request_elicitation = self.elicitation_handler.is_some(); let hooks_flag = self.hooks_handler.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() @@ -3693,9 +3734,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>, @@ -3903,19 +3950,7 @@ impl ResumeSessionConfig { let request_elicitation = self.elicitation_handler.is_some(); let hooks_flag = self.hooks_handler.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() @@ -4013,6 +4048,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 { @@ -4117,6 +4153,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, @@ -4727,11 +4764,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 +4932,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 +4967,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 +6414,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..61e7d7c44f 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -374,6 +374,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 f18e965428..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 [ @@ -78,6 +168,45 @@ fn host_start_round_trips_absent_and_legacy_child_pid() { } } +#[test] +fn host_start_round_trips_optional_transport_results() { + for wire in [ + serde_json::json!({"hostId": "host"}), + serde_json::json!({"hostId": "host", "environmentId": "environment"}), + serde_json::json!({ + "hostId": "host", "environmentId": "environment", + "url": "ws://127.0.0.1:4321", "token": "secret" + }), + ] { + let host: github_copilot_sdk::rpc::HostStartResult = + serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(host.url.as_deref(), wire["url"].as_str()); + assert_eq!( + host.environment_id.as_deref(), + wire["environmentId"].as_str() + ); + assert_eq!(host.token.as_deref(), wire["token"].as_str()); + assert_eq!(host.pid, None); + assert_eq!(serde_json::to_value(host).unwrap(), wire); + } +} + +#[test] +fn github_host_environment_requires_name_and_compute_id() { + use github_copilot_sdk::rpc::HostGitHubEnvironmentOptions; + + for wire in [ + serde_json::json!({}), + serde_json::json!({"name": "SDK host"}), + serde_json::json!({"computeId": "compute"}), + ] { + assert!(serde_json::from_value::(wire).is_err()); + } + let wire = serde_json::json!({"name": "SDK host", "computeId": "compute"}); + let options: HostGitHubEnvironmentOptions = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(options).unwrap(), wire); +} + #[test] fn operation_status_refuses_unknown_phases_and_incomplete_terminal_results() { let original = serde_json::json!({ @@ -324,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] @@ -337,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 0f150d3816..7e24053d61 100644 --- a/rust/tests/e2e.rs +++ b/rust/tests/e2e.rs @@ -7,8 +7,6 @@ mod abort; mod ask_user; #[path = "e2e/auto_tier.rs"] mod auto_tier; -#[path = "e2e/builtin_tools.rs"] -mod builtin_tools; #[path = "e2e/byok_bearer_token_provider.rs"] mod byok_bearer_token_provider; #[path = "e2e/canvas.rs"] @@ -23,8 +21,6 @@ mod client_lifecycle; mod client_options; #[path = "e2e/commands.rs"] mod commands; -#[path = "e2e/compaction.rs"] -mod compaction; #[path = "e2e/copilot_request_handler.rs"] mod copilot_request_handler; #[path = "e2e/elicitation.rs"] @@ -68,56 +64,8 @@ mod per_session_auth; mod permissions; #[path = "e2e/pre_mcp_tool_call_hook.rs"] mod pre_mcp_tool_call_hook; -#[path = "e2e/provider_endpoint.rs"] -mod provider_endpoint; -#[path = "e2e/rewind.rs"] -mod rewind; -#[path = "e2e/rpc_additional_edge_cases.rs"] -mod rpc_additional_edge_cases; -#[path = "e2e/rpc_agent.rs"] -mod rpc_agent; -#[path = "e2e/rpc_event_log.rs"] -mod rpc_event_log; -#[path = "e2e/rpc_event_side_effects.rs"] -mod rpc_event_side_effects; -#[path = "e2e/rpc_mcp_and_skills.rs"] -mod rpc_mcp_and_skills; -#[path = "e2e/rpc_mcp_config.rs"] -mod rpc_mcp_config; -#[path = "e2e/rpc_mcp_lifecycle.rs"] -mod rpc_mcp_lifecycle; -#[path = "e2e/rpc_queue.rs"] -mod rpc_queue; -#[path = "e2e/rpc_remote.rs"] -mod rpc_remote; -#[path = "e2e/rpc_schedule.rs"] -mod rpc_schedule; -#[path = "e2e/rpc_server.rs"] -mod rpc_server; -#[path = "e2e/rpc_server_misc.rs"] -mod rpc_server_misc; -#[path = "e2e/rpc_server_plugins.rs"] -mod rpc_server_plugins; -#[path = "e2e/rpc_server_remote_control.rs"] -mod rpc_server_remote_control; -#[path = "e2e/rpc_session_state.rs"] -mod rpc_session_state; -#[path = "e2e/rpc_session_state_extras.rs"] -mod rpc_session_state_extras; -#[path = "e2e/rpc_shell_and_fleet.rs"] -mod rpc_shell_and_fleet; -#[path = "e2e/rpc_shell_edge_cases.rs"] -mod rpc_shell_edge_cases; -#[path = "e2e/rpc_shell_user_requested.rs"] -mod rpc_shell_user_requested; -#[path = "e2e/rpc_surface_coverage.rs"] -mod rpc_surface_coverage; -#[path = "e2e/rpc_tasks_and_handlers.rs"] -mod rpc_tasks_and_handlers; -#[path = "e2e/rpc_ui_ephemeral_query.rs"] -mod rpc_ui_ephemeral_query; -#[path = "e2e/rpc_workspace_checkpoints.rs"] -mod rpc_workspace_checkpoints; +#[path = "e2e/rpc_smoke.rs"] +mod rpc_smoke; #[path = "e2e/session.rs"] mod session; #[path = "e2e/session_config.rs"] @@ -130,6 +78,8 @@ 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/skills.rs"] mod skills; #[path = "e2e/streaming_fidelity.rs"] diff --git a/rust/tests/e2e/builtin_tools.rs b/rust/tests/e2e/builtin_tools.rs deleted file mode 100644 index 12bcad4fa8..0000000000 --- a/rust/tests/e2e/builtin_tools.rs +++ /dev/null @@ -1,261 +0,0 @@ -use std::time::Duration; - -use github_copilot_sdk::MessageOptions; - -use super::support::assistant_message_content; - -/// Built-in tool tests spawn a real CLI subprocess and execute actual shell / -/// file tools. Under concurrent Windows CI load (e2e runs 4-wide on a 4-vCPU -/// runner) this agent loop can briefly exceed the 60s `send_and_wait` default, -/// so give it extra headroom while still failing fast on a genuine hang. -const SEND_TIMEOUT: Duration = Duration::from_secs(120); - -fn message(prompt: &str) -> MessageOptions { - MessageOptions::from(prompt).with_wait_timeout(SEND_TIMEOUT) -} - -#[tokio::test] -async fn should_capture_exit_code_in_output() { - super::support::with_shared_e2e_context( - &E2E, - "builtin_tools", - "should_capture_exit_code_in_output", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let msg = session - .send_and_wait(message( - "Run 'echo hello && echo world'. Tell me the exact output.", - )) - .await - .expect("send") - .expect("assistant message"); - let content = assistant_message_content(&msg); - assert!(content.contains("hello")); - assert!(content.contains("world")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_capture_stderr_output() { - super::support::with_shared_e2e_context(&E2E, "builtin_tools", "should_capture_stderr_output", |ctx| { - Box::pin(async move { - if cfg!(windows) { - return; - } - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let msg = session - .send_and_wait(message("Run 'echo error_msg >&2; sleep 0.5; echo ok' and tell me what stderr said. Reply with just the stderr content.")) - .await - .expect("send") - .expect("assistant message"); - assert!(assistant_message_content(&msg).contains("error_msg")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }) - .await; -} - -#[tokio::test] -async fn should_read_file_with_line_range() { - super::support::with_shared_e2e_context(&E2E, "builtin_tools", "should_read_file_with_line_range", |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - std::fs::write(ctx.work_dir().join("lines.txt"), "line1\nline2\nline3\nline4\nline5\n") - .expect("write lines file"); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let msg = session - .send_and_wait(message("Read lines 2 through 4 of the file 'lines.txt' in this directory. Tell me what those lines contain.")) - .await - .expect("send") - .expect("assistant message"); - let content = assistant_message_content(&msg); - assert!(content.contains("line2")); - assert!(content.contains("line4")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }) - .await; -} - -#[tokio::test] -async fn should_handle_nonexistent_file_gracefully() { - super::support::with_shared_e2e_context(&E2E, - "builtin_tools", - "should_handle_nonexistent_file_gracefully", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let msg = session - .send_and_wait(message("Try to read the file 'does_not_exist.txt'. If it doesn't exist, say 'FILE_NOT_FOUND'.")) - .await - .expect("send") - .expect("assistant message"); - let content = assistant_message_content(&msg).to_uppercase(); - assert!( - content.contains("NOT FOUND") - || content.contains("NOT EXIST") - || content.contains("NO SUCH") - || content.contains("FILE_NOT_FOUND") - || content.contains("DOES NOT EXIST") - || content.contains("ERROR"), - "expected missing-file response, got: {content}" - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_edit_a_file_successfully() { - super::support::with_shared_e2e_context(&E2E, "builtin_tools", "should_edit_a_file_successfully", |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - std::fs::write(ctx.work_dir().join("edit_me.txt"), "Hello World\nGoodbye World\n") - .expect("write edit file"); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let msg = session - .send_and_wait(message("Edit the file 'edit_me.txt': replace 'Hello World' with 'Hi Universe'. Then read it back and tell me its contents.")) - .await - .expect("send") - .expect("assistant message"); - assert!(assistant_message_content(&msg).contains("Hi Universe")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }) - .await; -} - -#[tokio::test] -async fn should_create_a_new_file() { - super::support::with_shared_e2e_context(&E2E, "builtin_tools", "should_create_a_new_file", |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let msg = session - .send_and_wait(message("Create a file called 'new_file.txt' with the content 'Created by test'. Then read it back to confirm.")) - .await - .expect("send") - .expect("assistant message"); - assert!(assistant_message_content(&msg).contains("Created by test")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }) - .await; -} - -#[tokio::test] -async fn should_search_for_patterns_in_files() { - super::support::with_shared_e2e_context(&E2E, - "builtin_tools", - "should_search_for_patterns_in_files", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - std::fs::write(ctx.work_dir().join("data.txt"), "apple\nbanana\napricot\ncherry\n") - .expect("write data file"); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let msg = session - .send_and_wait(message("Search for lines starting with 'ap' in the file 'data.txt'. Tell me which lines matched.")) - .await - .expect("send") - .expect("assistant message"); - let content = assistant_message_content(&msg); - assert!(content.contains("apple")); - assert!(content.contains("apricot")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_find_files_by_pattern() { - super::support::with_shared_e2e_context(&E2E, "builtin_tools", "should_find_files_by_pattern", |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let src = ctx.work_dir().join("src"); - std::fs::create_dir(&src).expect("create src directory"); - std::fs::write(src.join("index.ts"), "export const index = 1;") - .expect("write index.ts"); - std::fs::write(ctx.work_dir().join("README.md"), "# Readme").expect("write readme"); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let msg = session - .send_and_wait(message("Find all .ts files in this directory (recursively). List the filenames you found.")) - .await - .expect("send") - .expect("assistant message"); - assert!(assistant_message_content(&msg).contains("index.ts")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }) - .await; -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("builtin_tools", 8); diff --git a/rust/tests/e2e/canvas.rs b/rust/tests/e2e/canvas.rs index b0fd4c5111..a4fab77e5f 100644 --- a/rust/tests/e2e/canvas.rs +++ b/rust/tests/e2e/canvas.rs @@ -545,6 +545,7 @@ async fn resumed_canvas_reattaches_and_routes_all_callbacks() { .into_iter() .next() .expect("declared canvas"); + let mut events = session.subscribe(); session .rpc() .canvas() @@ -556,8 +557,16 @@ async fn resumed_canvas_reattaches_and_routes_all_callbacks() { }) .await .expect("open canvas"); + tokio::time::timeout(Duration::from_secs(10), async { + while session.open_canvases().is_empty() { + events.recv().await.expect("initial canvas opened event"); + } + }) + .await + .expect("initial canvas snapshot"); let snapshots = session.open_canvases(); assert_eq!(snapshots.len(), 1); + assert_eq!(snapshots[0].instance_id, "counter-resume"); session.rpc().suspend().await.expect("suspend session"); session.stop_event_loop().await; @@ -642,6 +651,13 @@ async fn resumed_canvas_reattaches_and_routes_all_callbacks() { "close:counter-resume" ] ); + tokio::time::timeout(Duration::from_secs(10), async { + while !resumed.open_canvases().is_empty() { + events.recv().await.expect("closed canvas event"); + } + }) + .await + .expect("closed canvas snapshot"); assert!(resumed.open_canvases().is_empty()); resumed 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 3fcf91efa2..68b23198a6 100644 --- a/rust/tests/e2e/commands.rs +++ b/rust/tests/e2e/commands.rs @@ -2,9 +2,9 @@ use std::sync::Arc; use async_trait::async_trait; use github_copilot_sdk::rpc::{ - CommandsInvokeRequest, CommandsListRequest, CommandsRespondToQueuedCommandRequest, - EnqueueCommandParams, EnqueueCommandResult, ExecuteCommandParams, RegisterEventInterestParams, - ReleaseEventInterestParams, SlashCommandInvocationResult, SlashCommandKind, + CommandsListRequest, CommandsRespondToQueuedCommandRequest, EnqueueCommandParams, + EnqueueCommandResult, ExecuteCommandParams, RegisterEventInterestParams, + ReleaseEventInterestParams, SlashCommandKind, }; use github_copilot_sdk::session_events::{CommandQueuedData, SessionEventType}; use github_copilot_sdk::{CommandContext, CommandDefinition, CommandHandler, RequestId}; @@ -84,53 +84,6 @@ async fn session_commands_list_returns_builtins_and_respects_client_command_filt .await; } -#[tokio::test] -async fn session_commands_invoke_known_builtin_returns_expected_result() { - super::support::with_shared_e2e_context( - &E2E, - "commands", - "session_with_no_commands_creates_successfully", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let result = session - .rpc() - .commands() - .invoke(CommandsInvokeRequest { - name: "context".to_string(), - input: None, - ..Default::default() - }) - .await - .expect("invoke context"); - match result { - SlashCommandInvocationResult::Text(text) => { - assert!(!text.text.trim().is_empty()); - } - SlashCommandInvocationResult::SelectSubcommand(select) => { - assert!(!select.options.is_empty()); - } - SlashCommandInvocationResult::AgentPrompt(prompt) => { - assert!(!prompt.prompt.trim().is_empty()); - } - SlashCommandInvocationResult::Completed(_) => {} - unexpected => panic!("unexpected command result: {unexpected:?}"), - } - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - #[tokio::test] async fn session_commands_execute_runs_registered_command_handler() { super::support::with_shared_e2e_context( @@ -300,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/compaction.rs b/rust/tests/e2e/compaction.rs deleted file mode 100644 index d56687d5f4..0000000000 --- a/rust/tests/e2e/compaction.rs +++ /dev/null @@ -1,116 +0,0 @@ -use github_copilot_sdk::rpc::{LogRequest, SessionLogLevel}; - -#[tokio::test] -async fn should_return_empty_handoff_summary_for_fresh_session() { - super::support::with_shared_e2e_context( - &E2E, - "compaction", - "should_return_empty_handoff_summary_for_fresh_session", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let summary = session - .rpc() - .history() - .summarize_for_handoff() - .await - .expect("summarize fresh session"); - assert!(summary.summary.is_empty()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_noop_when_cancelling_compaction_without_inflight_work() { - super::support::with_shared_e2e_context( - &E2E, - "compaction", - "should_report_noop_when_cancelling_compaction_without_inflight_work", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let cancelled = session - .rpc() - .history() - .cancel_background_compaction() - .await - .expect("cancel background compaction"); - assert!(!cancelled.cancelled); - let aborted = session - .rpc() - .history() - .abort_manual_compaction() - .await - .expect("abort manual compaction"); - assert!(!aborted.aborted); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_summarize_for_handoff_after_non_ephemeral_log_event() { - super::support::with_shared_e2e_context( - &E2E, - "compaction", - "should_summarize_for_handoff_after_non_ephemeral_log_event", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let log = session - .rpc() - .log(LogRequest { - ephemeral: Some(false), - level: Some(SessionLogLevel::Info), - message: "Rust handoff summary source".to_string(), - tip: None, - r#type: Some("notification".to_string()), - url: None, - }) - .await - .expect("log handoff source"); - assert!(!log.event_id.trim().is_empty()); - let summary = session - .rpc() - .history() - .summarize_for_handoff() - .await - .expect("summarize after log"); - assert!(summary.summary.is_empty() || summary.summary.contains("Rust")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("compaction", 3); diff --git a/rust/tests/e2e/mcp_oauth.rs b/rust/tests/e2e/mcp_oauth.rs index ba98aec7c8..ef19ef2d7a 100644 --- a/rust/tests/e2e/mcp_oauth.rs +++ b/rust/tests/e2e/mcp_oauth.rs @@ -15,7 +15,6 @@ use serde::Deserialize; use serde_json::Value; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::{Child, Command}; -use tokio::sync::Notify; use super::support::{wait_for_condition, with_e2e_context_no_snapshot}; @@ -310,124 +309,6 @@ async fn should_cancel_pending_mcp_oauth_request() { .await; } -#[tokio::test] -async fn should_resolve_pending_mcp_oauth_request_through_rpc() { - if super::support::skip_inprocess( - "blocked on github/copilot-agent-runtime#18961 MCP OAuth connection stall", - ) { - return; - } - with_e2e_context_no_snapshot(|ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let mut oauth_server = OAuthMcpServer::start( - ctx.repo_root() - .join("test/harness/test-mcp-oauth-server.mjs"), - ) - .await; - let server_name = "oauth-direct-rpc-mcp"; - let observed_request = Arc::new(Mutex::new(None)); - let request_observed = Arc::new(Notify::new()); - let release_handler = Arc::new(Notify::new()); - let handler = Arc::new(BlockingAuthHandler { - request: observed_request.clone(), - request_observed: request_observed.clone(), - release: release_handler.clone(), - }); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_enable_mcp_apps(true) - .with_mcp_auth_handler(handler) - .with_mcp_servers(IndexMap::from([( - server_name.to_string(), - McpServerConfig::Http(McpHttpServerConfig { - tools: Some(vec!["*".to_string()]), - timeout: None, - url: format!("{}/mcp", oauth_server.url), - headers: HashMap::new(), - }), - )])), - ) - .await - .expect("create session"); - - let connected = - wait_for_mcp_server_status(&session, server_name, McpServerStatus::Connected); - tokio::pin!(connected); - tokio::select! { - () = request_observed.notified() => {} - () = &mut connected => panic!("MCP server connected before OAuth request was observed"), - } - let request = observed_request - .lock() - .clone() - .expect("MCP auth request"); - assert_eq!(request.server_name, server_name); - assert_eq!(request.server_url, format!("{}/mcp", oauth_server.url)); - assert_eq!(request.reason, McpOauthRequestReason::Initial); - let www_authenticate = request - .www_authenticate_params - .as_ref() - .expect("WWW-Authenticate params"); - assert_eq!( - www_authenticate.resource_metadata_url, - Some(format!( - "{}/.well-known/oauth-protected-resource", - oauth_server.url - )) - ); - assert_eq!(www_authenticate.scope.as_deref(), Some("mcp.read")); - assert_eq!(www_authenticate.error.as_deref(), Some("invalid_token")); - - let handled = session - .rpc() - .mcp() - .oauth() - .handle_pending_request(github_copilot_sdk::rpc::McpOauthHandlePendingRequest { - request_id: request.request_id, - result: github_copilot_sdk::rpc::McpOauthPendingRequestResponse::Token( - github_copilot_sdk::rpc::McpOauthPendingRequestResponseToken { - access_token: EXPECTED_TOKEN.to_string(), - expires_in: Some(3600), - kind: github_copilot_sdk::rpc::McpOauthPendingRequestResponseTokenKind::Token, - token_type: Some("Bearer".to_string()), - }, - ), - }) - .await - .expect("handle pending MCP OAuth request"); - assert!(handled.success); - - release_handler.notify_one(); - connected.await; - let tools = session - .rpc() - .mcp() - .list_tools(McpListToolsRequest { - server_name: server_name.to_string(), - }) - .await - .expect("list MCP tools"); - assert!(tools.tools.iter().any(|tool| tool.name == "whoami")); - let requests = oauth_server.requests().await; - assert!( - requests - .iter() - .any(|request| { - request.authorization.as_deref() == Some(&format!("Bearer {EXPECTED_TOKEN}")) - }) - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - oauth_server.stop().await; - }) - }) - .await; -} - #[derive(Default)] struct TokenAuthHandler { request: Mutex>, @@ -534,32 +415,6 @@ impl McpAuthHandler for CancelAuthHandler { } } -struct BlockingAuthHandler { - request: Arc>>, - request_observed: Arc, - release: Arc, -} - -#[async_trait] -impl McpAuthHandler for BlockingAuthHandler { - async fn handle( - &self, - _session_id: SessionId, - request_id: RequestId, - request: McpAuthRequest, - ) -> McpAuthResult { - assert_eq!(request.request_id, request_id); - *self.request.lock() = Some(request); - self.request_observed.notify_one(); - self.release.notified().await; - McpAuthResult::Token { - access_token: EXPECTED_TOKEN.to_string(), - token_type: Some("Bearer".to_string()), - expires_in: Some(3600), - } - } -} - #[derive(Deserialize)] struct OAuthMcpRequest { authorization: Option, diff --git a/rust/tests/e2e/provider_endpoint.rs b/rust/tests/e2e/provider_endpoint.rs deleted file mode 100644 index 6aeff7b1a7..0000000000 --- a/rust/tests/e2e/provider_endpoint.rs +++ /dev/null @@ -1,226 +0,0 @@ -use std::collections::HashMap; -use std::ffi::OsString; -use std::sync::Arc; - -use github_copilot_sdk::handler::ApproveAllHandler; -use github_copilot_sdk::rpc::{ProviderEndpointType, ProviderEndpointWireApi}; -use github_copilot_sdk::{ProviderConfig, SessionConfig}; - -use super::support::{DEFAULT_TEST_TOKEN, with_e2e_context}; - -// session.provider.getEndpoint is gated behind COPILOT_ALLOW_GET_PROVIDER_ENDPOINT; -// the harness env passed to the CLI subprocess opts in for these tests. -fn opt_in_env() -> (OsString, OsString) { - ("COPILOT_ALLOW_GET_PROVIDER_ENDPOINT".into(), "true".into()) -} - -#[tokio::test] -#[allow(deprecated)] -async fn byok_provider_endpoint_returns_configured_endpoint() { - with_e2e_context( - "provider-endpoint", - "byok_provider_endpoint_returns_configured_endpoint", - |ctx| { - Box::pin(async move { - let mut options = ctx.client_options(); - if !super::support::is_inprocess_default() { - options.env.push(opt_in_env()); - } - let client = github_copilot_sdk::Client::start(options) - .await - .expect("start client"); - - let mut headers = HashMap::new(); - headers.insert("X-Custom-Header".to_string(), "byok-yes".to_string()); - - let session = client - .create_session( - SessionConfig::default() - .with_permission_handler(Arc::new(ApproveAllHandler)) - .with_provider( - ProviderConfig::new("https://api.example.test/v1") - .with_provider_type("openai") - .with_wire_api("completions") - .with_api_key("byok-secret") - .with_headers(headers), - ), - ) - .await - .expect("create session"); - - let endpoint = session - .rpc() - .provider() - .get_endpoint() - .await - .expect("get_endpoint"); - - assert!( - matches!(endpoint.r#type, ProviderEndpointType::Openai), - "expected type=openai, got {:?}", - endpoint.r#type, - ); - assert!( - matches!( - endpoint.wire_api, - Some(ProviderEndpointWireApi::Completions) - ), - "expected wireApi=completions, got {:?}", - endpoint.wire_api, - ); - assert_eq!(endpoint.base_url, "https://api.example.test/v1"); - assert_eq!(endpoint.api_key.as_deref(), Some("byok-secret")); - assert_eq!( - endpoint.headers.get("X-Custom-Header").map(String::as_str), - Some("byok-yes"), - ); - assert!( - endpoint.session_token.is_none(), - "BYOK sessions never issue a CAPI session token", - ); - - // disconnect may fail since the BYOK provider URL is fake - let _ = session.disconnect().await; - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -#[allow(deprecated)] -async fn capi_provider_endpoint_returns_resolved_credentials() { - // TODO(cli-1.0.81-2): CLI 1.0.81-2 stopped honoring client-level GitHub tokens over the - // in-process (FFI) host, which resolves auth from the ambient environment instead. - if super::support::skip_inprocess("client-level GitHub tokens are not supported in-process") { - return; - } - with_e2e_context( - "provider-endpoint", - "capi_provider_endpoint_returns_resolved_credentials", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let mut options = ctx.client_options_with_github_token(DEFAULT_TEST_TOKEN); - if !super::support::is_inprocess_default() { - options.env.push(opt_in_env()); - } - let client = github_copilot_sdk::Client::start(options) - .await - .expect("start client"); - - let session = client - .create_session( - SessionConfig::default() - .with_permission_handler(Arc::new(ApproveAllHandler)), - ) - .await - .expect("create session"); - - let endpoint = session - .rpc() - .provider() - .get_endpoint() - .await - .expect("get_endpoint"); - - assert!( - matches!( - endpoint.r#type, - ProviderEndpointType::Openai - | ProviderEndpointType::Azure - | ProviderEndpointType::Anthropic - ), - "expected type in {{openai, azure, anthropic}}, got {:?}", - endpoint.r#type, - ); - if !matches!(endpoint.r#type, ProviderEndpointType::Anthropic) { - assert!( - matches!( - endpoint.wire_api, - Some(ProviderEndpointWireApi::Completions) - | Some(ProviderEndpointWireApi::Responses) - ), - "expected wireApi in {{completions, responses}}, got {:?}", - endpoint.wire_api, - ); - } - - assert!( - endpoint.base_url.starts_with("http://") - || endpoint.base_url.starts_with("https://"), - "expected http(s) baseUrl, got {}", - endpoint.base_url, - ); - - let api_key = endpoint - .api_key - .as_deref() - .expect("CAPI OAuth session must surface apiKey"); - assert!(!api_key.is_empty(), "apiKey must be non-empty"); - - let integration_id = endpoint - .headers - .get("Copilot-Integration-Id") - .expect("Copilot-Integration-Id header"); - assert!( - !integration_id.is_empty(), - "Copilot-Integration-Id must be non-empty", - ); - - let user_agent = endpoint - .headers - .get("User-Agent") - .expect("User-Agent header"); - assert!( - user_agent.to_ascii_lowercase().contains("copilot"), - "expected User-Agent to mention Copilot, got {user_agent}", - ); - - let api_version = endpoint - .headers - .get("X-GitHub-Api-Version") - .expect("X-GitHub-Api-Version header"); - assert!( - !api_version.is_empty(), - "X-GitHub-Api-Version must be non-empty", - ); - - let interaction_id = endpoint - .headers - .get("X-Interaction-Id") - .expect("X-Interaction-Id header"); - let hex_count = interaction_id - .chars() - .filter(|c| c.is_ascii_hexdigit() || *c == '-') - .count(); - assert!( - hex_count >= 8, - "expected X-Interaction-Id to look like a hex/uuid value, got {interaction_id}", - ); - - let authorization = endpoint - .headers - .get("Authorization") - .expect("Authorization header"); - assert_eq!(authorization, &format!("Bearer {api_key}")); - - if let Some(session_token) = endpoint.session_token.as_ref() { - assert_eq!(session_token.header, "Copilot-Session-Token"); - assert!( - !session_token.token.is_empty(), - "session token must be non-empty", - ); - if let Some(expires_at) = session_token.expires_at.as_deref() { - assert!(!expires_at.is_empty(), "expected non-empty expiresAt",); - } - } - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} diff --git a/rust/tests/e2e/rewind.rs b/rust/tests/e2e/rewind.rs deleted file mode 100644 index 485c389ece..0000000000 --- a/rust/tests/e2e/rewind.rs +++ /dev/null @@ -1,158 +0,0 @@ -use std::path::Path; -use std::time::Duration; - -use github_copilot_sdk::rpc::{ - HistoryListRewindPointsResult, HistoryPreviewRewindRequest, HistoryRewindMode, - HistoryRewindOutcome, HistoryRewindRequest, -}; - -use super::support::assistant_message_content; - -const FILE_NAME: &str = "rewind-sdk.txt"; -const ORIGINAL_FILE_CONTENT: &str = "Original rewind content"; -const PREPARED_FILE_CONTENT: &str = "Prepared rewind content"; -const FILE_CONTENT: &str = "SDK rewind content"; - -#[tokio::test] -async fn should_restore_tracked_file_and_conversation() { - super::support::with_shared_e2e_context( - &E2E, - "rewind", - "should_restore_tracked_file_and_conversation", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let file_path = ctx.work_dir().join(FILE_NAME); - std::fs::write(&file_path, ORIGINAL_FILE_CONTENT).expect("write original file"); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_model("claude-sonnet-5") - .with_enable_file_change_tracking(true), - ) - .await - .expect("create session"); - - let ready = session - .send_and_wait(format!( - "Use the edit tool to replace the exact contents of {FILE_NAME} from \ - {ORIGINAL_FILE_CONTENT} to {PREPARED_FILE_CONTENT}. After the tool \ - succeeds, reply with exactly SDK_REWIND_READY." - )) - .await - .expect("send readiness turn") - .expect("readiness response"); - assert_eq!(assistant_message_content(&ready), "SDK_REWIND_READY"); - assert_eq!( - std::fs::read_to_string(&file_path).expect("read prepared file"), - PREPARED_FILE_CONTENT - ); - - let response = session - .send_and_wait(format!( - "Use the edit tool to replace the exact contents of {FILE_NAME} from \ - {PREPARED_FILE_CONTENT} to {FILE_CONTENT}. After the tool succeeds, \ - reply with exactly SDK_REWIND_DONE." - )) - .await - .expect("send rewind setup prompt") - .expect("assistant message"); - assert_eq!(assistant_message_content(&response), "SDK_REWIND_DONE"); - assert_eq!( - std::fs::read_to_string(&file_path).expect("read tracked file"), - FILE_CONTENT - ); - - let rewind_points = wait_for_rewind_points(&session).await; - assert!(rewind_points.file_change_tracking_enabled); - assert_eq!(rewind_points.points.len(), 2); - let rewind_point = &rewind_points.points[1]; - assert!(rewind_point.turn_changed_files); - assert!(rewind_point.can_restore_files); - assert_eq!(rewind_point.file_count, 1); - - let preview = session - .rpc() - .history() - .preview_rewind(HistoryPreviewRewindRequest { - event_id: rewind_point.event_id.clone(), - }) - .await - .expect("preview rewind"); - assert!(preview.available); - assert_eq!(preview.files.len(), 1); - assert_same_path(&file_path, Path::new(&preview.files[0].path)); - - let rewind = session - .rpc() - .history() - .rewind(HistoryRewindRequest { - event_id: rewind_point.event_id.clone(), - mode: HistoryRewindMode::ConversationAndFiles, - }) - .await - .expect("rewind conversation and files"); - assert_eq!(rewind.outcome, HistoryRewindOutcome::Success); - assert!(rewind.events_removed.is_some_and(|count| count > 0)); - assert_eq!(rewind.restored_files.len(), 1); - assert_same_path(&file_path, Path::new(&rewind.restored_files[0])); - assert_eq!( - std::fs::read_to_string(&file_path).expect("read restored file"), - PREPARED_FILE_CONTENT - ); - - let events = session.get_events().await.expect("get events after rewind"); - assert!(events.iter().all(|event| event.id != rewind_point.event_id)); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -async fn wait_for_rewind_points( - session: &github_copilot_sdk::session::Session, -) -> HistoryListRewindPointsResult { - let deadline = tokio::time::Instant::now() + Duration::from_secs(30); - loop { - let result = session - .rpc() - .history() - .list_rewind_points() - .await - .expect("list rewind points"); - if result.unavailable_reason.is_none() - && result.points.len() == 2 - && result.points[1].turn_changed_files - && result.points[1].can_restore_files - && result.points[1].file_count == 1 - { - return result; - } - assert!( - tokio::time::Instant::now() < deadline, - "timed out waiting for a restorable rewind point: {result:?}" - ); - tokio::time::sleep(Duration::from_millis(100)).await; - } -} - -fn assert_same_path(expected: &Path, actual: &Path) { - let expected = expected.to_string_lossy(); - let actual = actual.to_string_lossy(); - if cfg!(windows) { - let expected = expected.replace('\\', "/"); - let actual = actual.replace('\\', "/"); - assert!( - expected.eq_ignore_ascii_case(&actual), - "expected path {expected:?}, got {actual:?}" - ); - } else { - assert_eq!(expected, actual); - } -} - -static E2E: super::support::SharedE2eGroup = super::support::SharedE2eGroup::standard("rewind", 1); diff --git a/rust/tests/e2e/rpc_additional_edge_cases.rs b/rust/tests/e2e/rpc_additional_edge_cases.rs deleted file mode 100644 index 26eb591f4f..0000000000 --- a/rust/tests/e2e/rpc_additional_edge_cases.rs +++ /dev/null @@ -1,573 +0,0 @@ -use github_copilot_sdk::rpc::{ - ModeSetRequest, NameSetRequest, PermissionsResetSessionApprovalsRequest, - PermissionsSetApproveAllRequest, PlanUpdateRequest, ShellExecRequest, ShellKillRequest, - WorkspacesCreateFileRequest, WorkspacesReadFileRequest, -}; -use github_copilot_sdk::session_events::SessionMode; - -use super::support::wait_for_condition; - -#[tokio::test] -async fn shell_exec_with_zero_timeout_does_not_kill_long_running_command() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_additional_edge_cases", - "shell_exec_with_zero_timeout_does_not_kill_long_running_command", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let marker_path = ctx.work_dir().join("shell-zero-timeout-marker.txt"); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let result = session - .rpc() - .shell() - .exec(ShellExecRequest { - command: delayed_marker_command(&marker_path), - cwd: Some(ctx.work_dir().display().to_string()), - timeout: Some(0), - }) - .await - .expect("execute shell command"); - - assert!(!result.process_id.trim().is_empty()); - wait_for_condition("zero-timeout shell marker", || async { - marker_path.exists() - }) - .await; - let killed = session - .rpc() - .shell() - .kill(ShellKillRequest { - process_id: result.process_id, - signal: None, - }) - .await - .expect("kill zero-timeout shell process"); - assert!(killed.killed); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn workspaces_create_file_with_empty_content_round_trips() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_additional_edge_cases", - "workspaces_create_file_with_empty_content_round_trips", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let path = "empty-rust.txt"; - - session - .rpc() - .workspaces() - .create_file(WorkspacesCreateFileRequest { - path: path.to_string(), - content: String::new(), - }) - .await - .expect("create file"); - let read = session - .rpc() - .workspaces() - .read_file(WorkspacesReadFileRequest { - path: path.to_string(), - }) - .await - .expect("read file"); - assert_eq!(read.content, ""); - let listed = session - .rpc() - .workspaces() - .list_files() - .await - .expect("list files"); - assert!(listed.files.iter().any(|file| file == path)); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn workspaces_create_file_with_unicode_content_round_trips() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_additional_edge_cases", - "workspaces_create_file_with_unicode_content_round_trips", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let path = "unicode-rust.txt"; - let payload = "Hello, 世界! 🚀✨ Привет\u{0000}end"; - - session - .rpc() - .workspaces() - .create_file(WorkspacesCreateFileRequest { - path: path.to_string(), - content: payload.to_string(), - }) - .await - .expect("create file"); - let read = session - .rpc() - .workspaces() - .read_file(WorkspacesReadFileRequest { - path: path.to_string(), - }) - .await - .expect("read file"); - assert_eq!(read.content, payload); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn workspaces_create_file_with_large_content_round_trips() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_additional_edge_cases", - "workspaces_create_file_with_large_content_round_trips", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let path = "large-rust.txt"; - let payload: String = (0..256 * 1024) - .map(|i| (b'a' + (i % 26) as u8) as char) - .collect(); - - session - .rpc() - .workspaces() - .create_file(WorkspacesCreateFileRequest { - path: path.to_string(), - content: payload.clone(), - }) - .await - .expect("create file"); - let read = session - .rpc() - .workspaces() - .read_file(WorkspacesReadFileRequest { - path: path.to_string(), - }) - .await - .expect("read file"); - assert_eq!(read.content.len(), payload.len()); - assert_eq!(read.content, payload); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn plan_update_with_empty_content_then_read_returns_empty() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_additional_edge_cases", - "plan_update_with_empty_content_then_read_returns_empty", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - session - .rpc() - .plan() - .update(PlanUpdateRequest { - content: String::new(), - }) - .await - .expect("update plan"); - let read = session.rpc().plan().read().await.expect("read plan"); - assert_eq!(read.content.as_deref(), Some("")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn plan_delete_when_none_exists_is_idempotent() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_additional_edge_cases", - "plan_delete_when_none_exists_is_idempotent", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - session.rpc().plan().delete().await.expect("delete plan"); - session - .rpc() - .plan() - .delete() - .await - .expect("delete plan again"); - let read = session.rpc().plan().read().await.expect("read plan"); - assert!(read.content.as_deref().unwrap_or_default().is_empty()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn mode_set_to_same_value_multiple_times_stays_stable() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_additional_edge_cases", - "mode_set_to_same_value_multiple_times_stays_stable", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - for _ in 0..3 { - session - .rpc() - .mode() - .set(ModeSetRequest { - mode: SessionMode::Plan, - ..Default::default() - }) - .await - .expect("set mode"); - } - assert_eq!( - session.rpc().mode().get().await.expect("get mode"), - SessionMode::Plan - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn name_set_with_unicode_round_trips() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_additional_edge_cases", - "name_set_with_unicode_round_trips", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let name = "セッション 名前 ☕ – test"; - - session - .rpc() - .name() - .set(NameSetRequest { - name: name.to_string(), - }) - .await - .expect("set name"); - let read = session.rpc().name().get().await.expect("get name"); - assert_eq!(read.name.as_deref(), Some(name)); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn usage_get_metrics_on_fresh_session_returns_zero_tokens() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_additional_edge_cases", - "usage_get_metrics_on_fresh_session_returns_zero_tokens", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let metrics = session.rpc().usage().get_metrics().await.expect("metrics"); - assert_eq!(metrics.last_call_input_tokens, 0); - assert_eq!(metrics.last_call_output_tokens, 0); - assert_eq!(metrics.total_user_requests, 0); - assert!(!metrics.session_start_time.is_empty()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn permissions_reset_session_approvals_on_fresh_session_is_noop() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_additional_edge_cases", - "permissions_reset_session_approvals_on_fresh_session_is_noop", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let result = session - .rpc() - .permissions() - .reset_session_approvals(PermissionsResetSessionApprovalsRequest::default()) - .await - .expect("reset approvals"); - assert!(result.success); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn permissions_set_approve_all_toggle_round_trips() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_additional_edge_cases", - "permissions_set_approve_all_toggle_round_trips", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - assert!( - session - .rpc() - .permissions() - .set_approve_all(PermissionsSetApproveAllRequest { - enabled: true, - source: None, - }) - .await - .expect("enable approve all") - .success - ); - assert!( - session - .rpc() - .permissions() - .set_approve_all(PermissionsSetApproveAllRequest { - enabled: true, - source: None, - }) - .await - .expect("enable approve all again") - .success - ); - assert!( - session - .rpc() - .permissions() - .set_approve_all(PermissionsSetApproveAllRequest { - enabled: false, - source: None, - }) - .await - .expect("disable approve all") - .success - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn workspaces_createfile_then_listfiles_returns_all_files() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_additional_edge_cases", - "workspaces_createfile_then_listfiles_returns_all_files", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - for path in ["b-rust.txt", "a-rust.txt", "c-rust.txt"] { - session - .rpc() - .workspaces() - .create_file(WorkspacesCreateFileRequest { - path: path.to_string(), - content: path.to_string(), - }) - .await - .expect("create workspace file"); - } - - let first = session - .rpc() - .workspaces() - .list_files() - .await - .expect("list files"); - let second = session - .rpc() - .workspaces() - .list_files() - .await - .expect("list files again"); - for files in [&first.files, &second.files] { - for expected in ["a-rust.txt", "b-rust.txt", "c-rust.txt"] { - assert!(files.iter().any(|file| file == expected)); - } - } - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn workspaces_getworkspace_returns_stable_result_across_calls() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_additional_edge_cases", - "workspaces_getworkspace_returns_stable_result_across_calls", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let first = session - .rpc() - .workspaces() - .get_workspace() - .await - .expect("get workspace"); - let second = session - .rpc() - .workspaces() - .get_workspace() - .await - .expect("get workspace again"); - - assert_eq!( - first.workspace.as_ref().map(|workspace| &workspace.id), - second.workspace.as_ref().map(|workspace| &workspace.id) - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[cfg(windows)] -fn delayed_marker_command(marker_path: &std::path::Path) -> String { - format!( - "ping 127.0.0.1 -n 3 >nul & echo done>\"{}\" & ping 127.0.0.1 -n 61 >nul", - marker_path.display() - ) -} - -#[cfg(not(windows))] -fn delayed_marker_command(marker_path: &std::path::Path) -> String { - format!( - "sh -c \"sleep 2; printf done > '{}'; sleep 60\"", - marker_path.display() - ) -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_additional_edge_cases", 13); diff --git a/rust/tests/e2e/rpc_agent.rs b/rust/tests/e2e/rpc_agent.rs deleted file mode 100644 index 24fbd30673..0000000000 --- a/rust/tests/e2e/rpc_agent.rs +++ /dev/null @@ -1,351 +0,0 @@ -use github_copilot_sdk::CustomAgentConfig; -use github_copilot_sdk::rpc::{AgentInfo, AgentSelectRequest}; -use github_copilot_sdk::session_events::SessionEventType; -use serde_json::json; - -use super::support::wait_for_event; - -#[tokio::test] -async fn should_list_available_custom_agents() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_agents", - "should_list_available_custom_agents", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_custom_agents(create_custom_agents()), - ) - .await - .expect("create session"); - - let result = session.rpc().agent().list().await.expect("agent list"); - assert_agent(&result.agents, "test-agent", "Test Agent", "A test agent"); - assert_agent( - &result.agents, - "another-agent", - "Another Agent", - "Another test agent", - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_return_null_when_no_agent_is_selected() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_agents", - "should_return_null_when_no_agent_is_selected", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_custom_agents([create_custom_agents().remove(0)]), - ) - .await - .expect("create session"); - - let value = client - .call( - "session.agent.getCurrent", - Some(json!({ "sessionId": session.id() })), - ) - .await - .expect("get current agent"); - assert!(value.get("agent").is_some_and(serde_json::Value::is_null)); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_select_and_get_current_agent() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_agents", - "should_select_and_get_current_agent", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_custom_agents([create_custom_agents().remove(0)]), - ) - .await - .expect("create session"); - - let selected = session - .rpc() - .agent() - .select(AgentSelectRequest { - name: "test-agent".to_string(), - }) - .await - .expect("select agent"); - assert_eq!(selected.agent.name, "test-agent"); - assert_eq!(selected.agent.display_name, "Test Agent"); - - let current = session - .rpc() - .agent() - .get_current() - .await - .expect("get selected agent"); - assert_eq!(current.agent.name, "test-agent"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_emit_subagent_selected_and_deselected_events() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_agents", - "should_emit_subagent_selected_and_deselected_events", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_custom_agents([create_custom_agents().remove(0)]), - ) - .await - .expect("create session"); - - let selected_event = - wait_for_event(session.subscribe(), "subagent selected", |event| { - event.parsed_type() == SessionEventType::SubagentSelected - }); - session - .rpc() - .agent() - .select(AgentSelectRequest { - name: "test-agent".to_string(), - }) - .await - .expect("select agent"); - let selected = selected_event.await; - assert_eq!( - selected - .data - .get("agentName") - .and_then(serde_json::Value::as_str), - Some("test-agent") - ); - assert_eq!( - selected - .data - .get("agentDisplayName") - .and_then(serde_json::Value::as_str), - Some("Test Agent") - ); - - let deselected_event = - wait_for_event(session.subscribe(), "subagent deselected", |event| { - event.parsed_type() == SessionEventType::SubagentDeselected - }); - session - .rpc() - .agent() - .deselect() - .await - .expect("deselect agent"); - deselected_event.await; - - let value = client - .call( - "session.agent.getCurrent", - Some(json!({ "sessionId": session.id() })), - ) - .await - .expect("get current agent after deselect"); - assert!(value.get("agent").is_some_and(serde_json::Value::is_null)); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_deselect_current_agent() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_agents", - "should_deselect_current_agent", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_custom_agents([create_custom_agents().remove(0)]), - ) - .await - .expect("create session"); - - session - .rpc() - .agent() - .select(AgentSelectRequest { - name: "test-agent".to_string(), - }) - .await - .expect("select agent"); - session - .rpc() - .agent() - .deselect() - .await - .expect("deselect agent"); - let value = client - .call( - "session.agent.getCurrent", - Some(json!({ "sessionId": session.id() })), - ) - .await - .expect("get current agent"); - assert!(value.get("agent").is_some_and(serde_json::Value::is_null)); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_return_empty_list_when_no_custom_agents_configured() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_agents", - "should_return_empty_list_when_no_custom_agents_configured", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let result = session.rpc().agent().list().await.expect("agent list"); - assert!(result.agents.is_empty()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_call_agent_reload() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_agents", - "should_call_agent_reload", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let reload_agent = CustomAgentConfig::new( - "reload-test-agent-rust", - "You are a reload test agent.", - ) - .with_display_name("Reload Test Agent") - .with_description("Used by the agent reload RPC test."); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_custom_agents([reload_agent.clone()]), - ) - .await - .expect("create session"); - - assert_agent( - &session - .rpc() - .agent() - .list() - .await - .expect("list before") - .agents, - "reload-test-agent-rust", - "Reload Test Agent", - "Used by the agent reload RPC test.", - ); - let reloaded = session.rpc().agent().reload().await.expect("reload agents"); - let current = session.rpc().agent().list().await.expect("list after"); - assert_eq!( - agent_names(&reloaded.agents), - agent_names(¤t.agents), - "reload result should match current list" - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -fn create_custom_agents() -> Vec { - vec![ - CustomAgentConfig::new("test-agent", "You are a test agent.") - .with_display_name("Test Agent") - .with_description("A test agent"), - CustomAgentConfig::new("another-agent", "You are another agent.") - .with_display_name("Another Agent") - .with_description("Another test agent"), - ] -} - -fn assert_agent(agents: &[AgentInfo], name: &str, display_name: &str, description: &str) { - let agent = agents - .iter() - .find(|agent| agent.name == name) - .unwrap_or_else(|| panic!("missing agent {name}; actual agents: {agents:?}")); - assert_eq!(agent.display_name, display_name); - assert_eq!(agent.description, description); -} - -fn agent_names(agents: &[AgentInfo]) -> Vec<&str> { - let mut names: Vec<_> = agents.iter().map(|agent| agent.name.as_str()).collect(); - names.sort_unstable(); - names -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_agents", 7); diff --git a/rust/tests/e2e/rpc_event_log.rs b/rust/tests/e2e/rpc_event_log.rs deleted file mode 100644 index cb367ed8a9..0000000000 --- a/rust/tests/e2e/rpc_event_log.rs +++ /dev/null @@ -1,327 +0,0 @@ -use github_copilot_sdk::rpc::{ - EventLogReadRequest, EventsCursorStatus, RegisterEventInterestParams, - ReleaseEventInterestParams, -}; -use github_copilot_sdk::session_events::{ - PlanChangedOperation, SessionEventType, SessionPlanChangedData, SessionTitleChangedData, -}; -use serde_json::json; - -#[tokio::test] -async fn should_read_persisted_events_from_beginning() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_event_log", - "should_read_persisted_events_from_beginning", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - session - .rpc() - .plan() - .update(github_copilot_sdk::rpc::PlanUpdateRequest { - content: "# event log plan".to_string(), - }) - .await - .expect("write plan"); - client - .rpc() - .sessions() - .save(github_copilot_sdk::rpc::SessionsSaveRequest { - session_id: session.id().clone(), - }) - .await - .expect("save session"); - - let read = session - .rpc() - .event_log() - .read(EventLogReadRequest { - agent_ids: None, - agent_scope: None, - cursor: None, - direction: None, - include_ephemeral: None, - max: Some(100), - types: Some(json!("*")), - wait_ms: Some(0), - }) - .await - .expect("read event log"); - assert_eq!(read.cursor_status, EventsCursorStatus::Ok); - assert!(!read.cursor.trim().is_empty()); - assert!(read.events.iter().any(|event| { - event.parsed_type() == SessionEventType::SessionPlanChanged - && event - .typed_data::() - .is_some_and(|data| data.operation == PlanChangedOperation::Create) - })); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_return_tail_cursor_and_read_empty_when_no_new_events() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_event_log", - "should_return_tail_cursor_and_read_empty_when_no_new_events", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - session - .rpc() - .plan() - .update(github_copilot_sdk::rpc::PlanUpdateRequest { - content: "# tail cursor plan".to_string(), - }) - .await - .expect("write plan"); - client - .rpc() - .sessions() - .save(github_copilot_sdk::rpc::SessionsSaveRequest { - session_id: session.id().clone(), - }) - .await - .expect("save session"); - let before_tail = session - .rpc() - .event_log() - .read(EventLogReadRequest { - agent_ids: None, - agent_scope: None, - cursor: None, - direction: None, - include_ephemeral: None, - max: Some(100), - types: Some(json!(["session.plan_changed"])), - wait_ms: Some(0), - }) - .await - .expect("read persisted plan event"); - assert!( - before_tail - .events - .iter() - .any(|event| event.parsed_type() == SessionEventType::SessionPlanChanged) - ); - - let tail = session.rpc().event_log().tail().await.expect("tail"); - assert!(!tail.cursor.trim().is_empty()); - let request = EventLogReadRequest { - agent_ids: None, - agent_scope: None, - cursor: Some(tail.cursor), - direction: None, - include_ephemeral: Some(false), - max: Some(10), - // Startup events can arrive after the tail; only the plan update is controlled here. - types: Some(json!(["session.plan_changed"])), - wait_ms: Some(0), - }; - let read = session - .rpc() - .event_log() - .read(request.clone()) - .await - .expect("read from tail"); - assert_eq!(read.cursor_status, EventsCursorStatus::Ok); - assert!( - read.events.is_empty(), - "unexpected events after tail: {:?}", - read.events - ); - assert!(!read.has_more); - - session - .rpc() - .plan() - .update(github_copilot_sdk::rpc::PlanUpdateRequest { - content: "# durable event after tail".to_string(), - }) - .await - .expect("write plan"); - client - .rpc() - .sessions() - .save(github_copilot_sdk::rpc::SessionsSaveRequest { - session_id: session.id().clone(), - }) - .await - .expect("save session"); - let read = session - .rpc() - .event_log() - .read(request) - .await - .expect("read new durable event from tail"); - assert_eq!(read.cursor_status, EventsCursorStatus::Ok); - assert!( - read.events - .iter() - .any(|event| event.parsed_type() == SessionEventType::SessionPlanChanged), - "new durable event missing after tail: {:?}", - read.events - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_register_and_release_event_interest_idempotently() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_event_log", - "should_register_and_release_event_interest_idempotently", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let handle = session - .rpc() - .event_log() - .register_interest(RegisterEventInterestParams { - event_type: "session.title_changed".to_string(), - }) - .await - .expect("register interest") - .handle; - assert!(!handle.trim().is_empty()); - for _ in 0..2 { - assert!( - session - .rpc() - .event_log() - .release_interest(ReleaseEventInterestParams { - handle: handle.clone(), - }) - .await - .expect("release interest") - .success - ); - } - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_longpoll_with_types_filter_for_titlechanged_event() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_event_log", - "should_longpoll_with_types_filter_for_titlechanged_event", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let tail = session.rpc().event_log().tail().await.expect("tail"); - let event_log = session.rpc().event_log(); - let mut request = EventLogReadRequest { - agent_ids: None, - agent_scope: None, - cursor: Some(tail.cursor), - direction: None, - include_ephemeral: None, - max: Some(10), - types: Some(json!(["session.title_changed"])), - wait_ms: Some(5_000), - }; - let read_future = event_log.read(request.clone()); - let write_future = async { - session - .rpc() - .plan() - .update(github_copilot_sdk::rpc::PlanUpdateRequest { - content: "# Unrelated event during a filtered read".to_string(), - }) - .await - .expect("write unrelated plan"); - }; - let (read, _) = tokio::join!(read_future, write_future); - let mut read = read.expect("long-poll event log"); - assert!( - read.events.is_empty(), - "unexpected events after unrelated plan update: {:?}", - read.events - ); - session - .rpc() - .name() - .set(github_copilot_sdk::rpc::NameSetRequest { - name: "Rust event log title".to_string(), - }) - .await - .expect("set title"); - tokio::time::timeout(std::time::Duration::from_secs(10), async { - loop { - assert_eq!(read.cursor_status, EventsCursorStatus::Ok); - assert!(read.events.iter().all(|event| { - event.parsed_type() == SessionEventType::SessionTitleChanged - })); - if read.events.iter().any(|event| { - event - .typed_data::() - .is_some_and(|data| data.title == "Rust event log title") - }) { - break; - } - request.cursor = Some(read.cursor.clone()); - read = event_log - .read(request.clone()) - .await - .expect("continue filtered event log read"); - } - }) - .await - .unwrap_or_else(|_| { - panic!( - "timed out waiting for filtered title event; last batch: {:?}", - read.events - ) - }); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_event_log", 4); diff --git a/rust/tests/e2e/rpc_event_side_effects.rs b/rust/tests/e2e/rpc_event_side_effects.rs deleted file mode 100644 index 06adc3f65c..0000000000 --- a/rust/tests/e2e/rpc_event_side_effects.rs +++ /dev/null @@ -1,363 +0,0 @@ -use github_copilot_sdk::rpc::{ - HistoryTruncateRequest, ModeSetRequest, NameSetRequest, PlanUpdateRequest, - WorkspacesCreateFileRequest, -}; -use github_copilot_sdk::session_events::{ - PlanChangedOperation, SessionEventType, SessionMode, SessionModeChangedData, - SessionPlanChangedData, SessionSnapshotRewindData, SessionTitleChangedData, - SessionWorkspaceFileChangedData, -}; - -use super::support::{assistant_message_content, wait_for_event}; - -#[tokio::test] -async fn should_emit_mode_changed_event_when_mode_set() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_event_side_effects", - "should_emit_mode_changed_event_when_mode_set", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let changed = wait_for_event(session.subscribe(), "mode changed", |event| { - if event.parsed_type() != SessionEventType::SessionModeChanged { - return false; - } - let data = event - .typed_data::() - .expect("mode changed data"); - data.previous_mode == SessionMode::Interactive - && data.new_mode == SessionMode::Plan - }); - session - .rpc() - .mode() - .set(ModeSetRequest { - mode: SessionMode::Plan, - ..Default::default() - }) - .await - .expect("set mode"); - changed.await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_emit_plan_changed_event_for_update_and_delete() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_event_side_effects", - "should_emit_plan_changed_event_for_update_and_delete", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let create = wait_for_plan_event(&session, PlanChangedOperation::Create); - session - .rpc() - .plan() - .update(PlanUpdateRequest { - content: "# Test plan\n- item".to_string(), - }) - .await - .expect("create plan"); - create.await; - - let delete = wait_for_plan_event(&session, PlanChangedOperation::Delete); - session.rpc().plan().delete().await.expect("delete plan"); - delete.await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_emit_plan_changed_update_operation_on_second_update() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_event_side_effects", - "should_emit_plan_changed_update_operation_on_second_update", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - session - .rpc() - .plan() - .update(PlanUpdateRequest { - content: "# initial".to_string(), - }) - .await - .expect("create plan"); - let update = wait_for_plan_event(&session, PlanChangedOperation::Update); - session - .rpc() - .plan() - .update(PlanUpdateRequest { - content: "# updated".to_string(), - }) - .await - .expect("update plan"); - update.await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_emit_workspace_file_changed_event_when_file_created() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_event_side_effects", - "should_emit_workspace_file_changed_event_when_file_created", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let path = "side-effect-rust.txt"; - - let changed = - wait_for_event(session.subscribe(), "workspace file changed", |event| { - if event.parsed_type() != SessionEventType::SessionWorkspaceFileChanged { - return false; - } - event - .typed_data::() - .expect("workspace file changed data") - .path - == path - }); - session - .rpc() - .workspaces() - .create_file(WorkspacesCreateFileRequest { - path: path.to_string(), - content: "hello".to_string(), - }) - .await - .expect("create workspace file"); - changed.await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_emit_title_changed_event_when_name_set() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_event_side_effects", - "should_emit_title_changed_event_when_name_set", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let title = "Renamed-Rust"; - - let changed = wait_for_event(session.subscribe(), "title changed", |event| { - if event.parsed_type() != SessionEventType::SessionTitleChanged { - return false; - } - event - .typed_data::() - .expect("title changed data") - .title - == title - }); - session - .rpc() - .name() - .set(NameSetRequest { - name: title.to_string(), - }) - .await - .expect("set name"); - changed.await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_emit_snapshot_rewind_event_and_remove_events_on_truncate() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_event_side_effects", - "should_emit_snapshot_rewind_event_and_remove_events_on_truncate", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let answer = session - .send_and_wait("Say SNAPSHOT_REWIND_TARGET exactly.") - .await - .expect("send") - .expect("assistant message"); - assert!(assistant_message_content(&answer).contains("SNAPSHOT_REWIND_TARGET")); - let user_event = session - .get_events() - .await - .expect("messages") - .into_iter() - .find(|event| event.parsed_type() == SessionEventType::UserMessage) - .expect("user.message event"); - let target_event_id = user_event.id.clone(); - - let rewind = wait_for_event(session.subscribe(), "snapshot rewind", |event| { - if event.parsed_type() != SessionEventType::SessionSnapshotRewind { - return false; - } - event - .typed_data::() - .expect("snapshot rewind data") - .up_to_event_id - == target_event_id - }); - let result = session - .rpc() - .history() - .truncate(HistoryTruncateRequest { - event_id: target_event_id.clone(), - }) - .await - .expect("truncate history"); - assert!(result.events_removed >= 1); - rewind.await; - - let remaining = session.get_events().await.expect("messages after truncate"); - assert!(!remaining.iter().any(|event| event.id == target_event_id)); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_allow_session_use_after_truncate() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_event_side_effects", - "should_allow_session_use_after_truncate", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - session - .send_and_wait("Say SNAPSHOT_REWIND_TARGET exactly.") - .await - .expect("send"); - let user_event = session - .get_events() - .await - .expect("messages") - .into_iter() - .find(|event| event.parsed_type() == SessionEventType::UserMessage) - .expect("user.message event"); - - let result = session - .rpc() - .history() - .truncate(HistoryTruncateRequest { - event_id: user_event.id, - }) - .await - .expect("truncate history"); - assert!(result.events_removed >= 1); - session - .rpc() - .mode() - .get() - .await - .expect("mode after truncate"); - session - .rpc() - .workspaces() - .get_workspace() - .await - .expect("workspace after truncate"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -fn wait_for_plan_event( - session: &github_copilot_sdk::session::Session, - operation: PlanChangedOperation, -) -> impl std::future::Future { - let events = session.subscribe(); - wait_for_event(events, "plan changed", move |event| { - if event.parsed_type() != SessionEventType::SessionPlanChanged { - return false; - } - event - .typed_data::() - .expect("plan changed data") - .operation - == operation - }) -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_event_side_effects", 7); diff --git a/rust/tests/e2e/rpc_mcp_and_skills.rs b/rust/tests/e2e/rpc_mcp_and_skills.rs deleted file mode 100644 index 8cf3efd998..0000000000 --- a/rust/tests/e2e/rpc_mcp_and_skills.rs +++ /dev/null @@ -1,837 +0,0 @@ -use std::collections::HashMap; -use std::path::Path; - -use github_copilot_sdk::rpc::{ - ExtensionsDisableRequest, ExtensionsEnableRequest, McpAppsCallToolRequest, - McpAppsDiagnoseRequest, McpAppsListToolsRequest, McpAppsSetHostContextDetails, - McpAppsSetHostContextDetailsAvailableDisplayMode, McpAppsSetHostContextDetailsDisplayMode, - McpAppsSetHostContextDetailsPlatform, McpAppsSetHostContextDetailsTheme, - McpAppsSetHostContextRequest, McpCancelSamplingExecutionParams, McpDisableRequest, - McpEnableRequest, McpExecuteSamplingParams, McpExecuteSamplingRequest, McpOauthLoginRequest, - McpResourcesReadRequest, McpSamplingExecutionAction, McpSetEnvValueModeDetails, - McpSetEnvValueModeParams, PermissionsSetModeRequest, SkillsDisableRequest, SkillsEnableRequest, -}; -use github_copilot_sdk::session_events::PermissionMode; -use github_copilot_sdk::{IndexMap, McpServerConfig, McpStdioServerConfig}; - -#[tokio::test] -async fn should_list_and_toggle_session_skills() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_and_skills", - "should_list_and_toggle_session_skills", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let skill_name = "session-rpc-skill-rust"; - let skills_dir = create_skill_directory( - ctx.work_dir(), - skill_name, - "Session skill controlled by RPC.", - ); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_skill_directories([skills_dir]) - .with_disabled_skills([skill_name]), - ) - .await - .expect("create session"); - - assert_skill( - session.rpc().skills().list().await.expect("list disabled"), - skill_name, - false, - ); - session - .rpc() - .skills() - .enable(SkillsEnableRequest { - name: skill_name.to_string(), - }) - .await - .expect("enable skill"); - assert_skill( - session.rpc().skills().list().await.expect("list enabled"), - skill_name, - true, - ); - session - .rpc() - .skills() - .disable(SkillsDisableRequest { - name: skill_name.to_string(), - }) - .await - .expect("disable skill"); - assert_skill( - session - .rpc() - .skills() - .list() - .await - .expect("list disabled again"), - skill_name, - false, - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_ensure_skills_are_loaded_and_list_invoked_skills() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_and_skills", - "should_ensure_skills_are_loaded_and_list_invoked_skills", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let skill_name = "ensure-loaded-rpc-skill-rust"; - let skills_dir = create_skill_directory( - ctx.work_dir(), - skill_name, - "Skill available to ensureLoaded tests.", - ); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_skill_directories([skills_dir]), - ) - .await - .expect("create session"); - - session - .rpc() - .skills() - .ensure_loaded() - .await - .expect("ensure loaded"); - assert_skill( - session.rpc().skills().list().await.expect("list skills"), - skill_name, - true, - ); - let invoked = session - .rpc() - .skills() - .get_invoked() - .await - .expect("get invoked skills"); - assert!(invoked.skills.is_empty()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_reload_session_skills() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_and_skills", - "should_reload_session_skills", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let skills_dir = ctx.work_dir().join("reloadable-rpc-skills"); - std::fs::create_dir_all(&skills_dir).expect("create skills dir"); - let skill_name = "reload-rpc-skill-rust"; - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_skill_directories([skills_dir.clone()]), - ) - .await - .expect("create session"); - - let before = session.rpc().skills().list().await.expect("list before"); - assert!(!before.skills.iter().any(|skill| skill.name == skill_name)); - - create_skill( - &skills_dir, - skill_name, - "Skill added after session creation.", - ); - session - .rpc() - .skills() - .reload() - .await - .expect("reload skills"); - let after = session.rpc().skills().list().await.expect("list after"); - let skill = assert_skill(after, skill_name, true); - assert_eq!(skill.description, "Skill added after session creation."); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_list_mcp_servers_with_configured_server() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_and_skills", - "should_list_mcp_servers_with_configured_server", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let server_name = "rpc-list-mcp-server"; - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_mcp_servers(test_mcp_servers(ctx.repo_root(), server_name)), - ) - .await - .expect("create session"); - - let result = session.rpc().mcp().list().await.expect("mcp list"); - assert!( - result - .servers - .iter() - .any(|server| server.name == server_name) - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_set_mcp_env_value_mode_and_remove_github_server() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_and_skills", - "should_set_mcp_env_value_mode_and_remove_github_server", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let mode = session - .rpc() - .mcp() - .set_env_value_mode(McpSetEnvValueModeParams { - mode: McpSetEnvValueModeDetails::Direct, - }) - .await - .expect("set env value mode"); - assert_eq!(mode.mode, McpSetEnvValueModeDetails::Direct); - let removed = session - .rpc() - .mcp() - .remove_git_hub() - .await - .expect("remove github mcp"); - assert!(!removed.removed); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_mcp_sampling_failure_and_cancel_missing_sampling() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_and_skills", - "should_report_mcp_sampling_failure_and_cancel_missing_sampling", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - assert!( - !session - .rpc() - .mcp() - .cancel_sampling_execution(McpCancelSamplingExecutionParams { - request_id: "missing-sampling".into(), - }) - .await - .expect("cancel missing sampling") - .cancelled - ); - match session - .rpc() - .mcp() - .execute_sampling(McpExecuteSamplingParams { - mcp_request_id: serde_json::json!("sampling-request"), - request: McpExecuteSamplingRequest {}, - request_id: "sampling-request".into(), - server_name: "missing-server".to_string(), - }) - .await - { - Ok(result) => { - assert_ne!(result.action, McpSamplingExecutionAction::Success); - assert!(result.result.is_none()); - } - Err(err) => { - assert!( - !err.to_string() - .contains("Unhandled method session.mcp.executeSampling") - ); - } - } - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_list_plugins() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_and_skills", - "should_list_plugins", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let result = session.rpc().plugins().list().await.expect("plugins list"); - assert!( - result.plugins.iter().all(|plugin| !plugin.name.is_empty()), - "plugins should have names: {:?}", - result.plugins - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_list_extensions() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_and_skills", - "should_list_extensions", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - session - .rpc() - .permissions() - .set_mode(PermissionsSetModeRequest { - assisted_approval_model: None, - mode: PermissionMode::AllowAll, - source: None, - }) - .await - .expect("enable allow-all"); - - let result = session - .rpc() - .extensions() - .list() - .await - .expect("extensions list"); - assert!( - result - .extensions - .iter() - .all(|extension| !extension.id.is_empty() && !extension.name.is_empty()), - "extensions should have ids and names: {:?}", - result.extensions - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_round_trip_mcp_app_host_context() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_and_skills", - "should_round_trip_mcp_app_host_context", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - session - .rpc() - .mcp() - .apps() - .set_host_context(McpAppsSetHostContextRequest { - context: McpAppsSetHostContextDetails { - available_display_modes: Some(vec![ - McpAppsSetHostContextDetailsAvailableDisplayMode::Inline, - McpAppsSetHostContextDetailsAvailableDisplayMode::Fullscreen, - ]), - display_mode: Some(McpAppsSetHostContextDetailsDisplayMode::Inline), - locale: Some("en-US".to_string()), - platform: Some(McpAppsSetHostContextDetailsPlatform::Desktop), - theme: Some(McpAppsSetHostContextDetailsTheme::Dark), - time_zone: Some("Etc/UTC".to_string()), - user_agent: Some("rust-e2e".to_string()), - }, - }) - .await - .expect("set host context"); - let context = session - .rpc() - .mcp() - .apps() - .get_host_context() - .await - .expect("get host context") - .context; - assert_eq!(context.locale.as_deref(), Some("en-US")); - assert_eq!(context.time_zone.as_deref(), Some("Etc/UTC")); - assert_eq!(context.user_agent.as_deref(), Some("rust-e2e")); - assert_eq!( - context.available_display_modes.as_ref().map_or(0, Vec::len), - 2 - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_diagnose_and_report_mcp_app_capability_errors() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_and_skills", - "should_diagnose_and_report_mcp_app_capability_errors", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let server_name = "missing-app-server"; - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let diagnose = session - .rpc() - .mcp() - .apps() - .diagnose(McpAppsDiagnoseRequest { - server_name: server_name.to_string(), - }) - .await - .expect("diagnose mcp apps"); - assert!(!diagnose.server.connected); - assert_eq!(diagnose.server.tool_count, 0.0); - assert!(diagnose.server.sample_tool_names.is_empty()); - let _capability = diagnose.capability; - - expect_err_contains( - session - .rpc() - .mcp() - .apps() - .list_tools(McpAppsListToolsRequest { - server_name: server_name.to_string(), - origin_server_name: server_name.to_string(), - }), - "mcp", - ) - .await; - expect_err_contains( - session - .rpc() - .mcp() - .apps() - .call_tool(McpAppsCallToolRequest { - arguments: Some(HashMap::new()), - server_name: server_name.to_string(), - origin_server_name: server_name.to_string(), - tool_name: "missing-tool".to_string(), - }), - "mcp", - ) - .await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_error_when_mcp_app_resource_is_not_available() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_and_skills", - "should_report_error_when_mcp_app_resource_is_not_available", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let err = session - .rpc() - .mcp() - .resources() - .read(McpResourcesReadRequest { - server_name: "missing-app-server".to_string(), - uri: "ui://missing/resource.html".to_string(), - }) - .await - .expect_err("missing resource should fail"); - let message = err.to_string().to_ascii_lowercase(); - assert!( - message.contains("resource") - || message.contains("not found") - || message.contains("method not found") - || message.contains("mcp"), - "unexpected readResource error: {err}" - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_error_when_mcp_host_is_not_initialized() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_and_skills", - "should_report_error_when_mcp_host_is_not_initialized", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - expect_err_contains( - session.rpc().mcp().enable(McpEnableRequest { - server_name: "missing-server".to_string(), - }), - "No MCP host initialized", - ) - .await; - expect_err_contains( - session.rpc().mcp().disable(McpDisableRequest { - server_name: "missing-server".to_string(), - }), - "No MCP host initialized", - ) - .await; - expect_err_contains( - session.rpc().mcp().reload(), - "MCP config reload not available", - ) - .await; - expect_err_contains( - session.rpc().mcp().oauth().login(McpOauthLoginRequest { - server_name: "missing-server".to_string(), - callback_success_message: None, - client_name: None, - force_reauth: None, - client_id: None, - client_secret: None, - grant_type: None, - public_client: None, - }), - "MCP host is not available", - ) - .await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_error_when_mcp_oauth_server_is_not_configured() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_and_skills", - "should_report_error_when_mcp_oauth_server_is_not_configured", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config().with_mcp_servers( - test_mcp_servers(ctx.repo_root(), "configured-stdio-server"), - )) - .await - .expect("create session"); - - expect_err_contains( - session.rpc().mcp().oauth().login(McpOauthLoginRequest { - server_name: "missing-server".to_string(), - callback_success_message: None, - client_name: None, - force_reauth: None, - client_id: None, - client_secret: None, - grant_type: None, - public_client: None, - }), - "is not configured", - ) - .await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_error_when_mcp_oauth_server_is_not_remote() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_and_skills", - "should_report_error_when_mcp_oauth_server_is_not_remote", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let server_name = "configured-stdio-server"; - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_mcp_servers(test_mcp_servers(ctx.repo_root(), server_name)), - ) - .await - .expect("create session"); - - expect_err_contains( - session.rpc().mcp().oauth().login(McpOauthLoginRequest { - server_name: server_name.to_string(), - callback_success_message: Some("Done".to_string()), - client_name: Some("SDK E2E".to_string()), - force_reauth: Some(true), - client_id: None, - client_secret: None, - grant_type: None, - public_client: None, - }), - "not a remote server", - ) - .await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_error_when_extensions_are_not_available() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_and_skills", - "should_report_error_when_extensions_are_not_available", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - session - .rpc() - .permissions() - .set_mode(PermissionsSetModeRequest { - assisted_approval_model: None, - mode: PermissionMode::AllowAll, - source: None, - }) - .await - .expect("enable allow-all"); - - expect_err_contains( - session.rpc().extensions().enable(ExtensionsEnableRequest { - id: "missing-extension".to_string(), - }), - "Extensions not available", - ) - .await; - expect_err_contains( - session - .rpc() - .extensions() - .disable(ExtensionsDisableRequest { - id: "missing-extension".to_string(), - }), - "Extensions not available", - ) - .await; - expect_err_contains( - session.rpc().extensions().reload(), - "Extensions not available", - ) - .await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -fn create_skill_directory( - work_dir: &std::path::Path, - skill_name: &str, - description: &str, -) -> std::path::PathBuf { - let skills_dir = work_dir.join("session-rpc-skills"); - create_skill(&skills_dir, skill_name, description); - skills_dir -} - -fn create_skill(skills_dir: &std::path::Path, skill_name: &str, description: &str) { - let skill_dir = skills_dir.join(skill_name); - std::fs::create_dir_all(&skill_dir).expect("create skill dir"); - std::fs::write( - skill_dir.join("SKILL.md"), - format!( - "---\nname: {skill_name}\ndescription: {description}\n---\n\n# {skill_name}\n\nThis skill is used by RPC E2E tests.\n" - ), - ) - .expect("write skill"); -} - -fn assert_skill( - list: github_copilot_sdk::rpc::SkillList, - skill_name: &str, - enabled: bool, -) -> github_copilot_sdk::rpc::Skill { - let skill = list - .skills - .into_iter() - .find(|skill| skill.name == skill_name) - .unwrap_or_else(|| panic!("skill {skill_name} not found")); - assert_eq!(skill.enabled, enabled); - assert!( - skill - .path - .as_deref() - .is_some_and(|path| path.contains(skill_name) && path.ends_with("SKILL.md")) - ); - skill -} - -fn test_mcp_servers(repo_root: &Path, server_name: &str) -> IndexMap { - let harness_dir = repo_root.join("test").join("harness"); - let server_path = harness_dir - .join("test-mcp-server.mjs") - .to_string_lossy() - .to_string(); - - IndexMap::from([( - server_name.to_string(), - McpServerConfig::Stdio(McpStdioServerConfig { - tools: Some(vec!["*".to_string()]), - command: if cfg!(windows) { - "node.exe".to_string() - } else { - "node".to_string() - }, - args: vec![server_path], - working_directory: Some(harness_dir.to_string_lossy().to_string()), - ..McpStdioServerConfig::default() - }), - )]) -} - -async fn expect_err_contains( - future: impl std::future::Future>, - expected: &str, -) { - let err = match future.await { - Ok(_) => panic!("expected RPC failure"), - Err(err) => err, - }; - assert!( - err.to_string() - .to_ascii_lowercase() - .contains(&expected.to_ascii_lowercase()), - "expected error to contain {expected:?}, got {err}" - ); -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_mcp_and_skills", 15); diff --git a/rust/tests/e2e/rpc_mcp_config.rs b/rust/tests/e2e/rpc_mcp_config.rs deleted file mode 100644 index 29f5f42d39..0000000000 --- a/rust/tests/e2e/rpc_mcp_config.rs +++ /dev/null @@ -1,217 +0,0 @@ -use github_copilot_sdk::rpc::{ - McpConfigAddRequest, McpConfigDisableRequest, McpConfigEnableRequest, McpConfigRemoveRequest, - McpConfigUpdateRequest, -}; -use serde_json::json; - -#[tokio::test] -async fn should_call_server_mcp_config_rpcs() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_config", - "should_call_server_mcp_config_rpcs", - |ctx| { - Box::pin(async move { - let server_name = "rust-sdk-test-mcp-config"; - let client = ctx.start_client().await; - let config = client.rpc().mcp().config(); - let _ = config - .remove(McpConfigRemoveRequest { - name: server_name.to_string(), - auth_client_id_metadata_url: None, - }) - .await; - - let initial = config.list().await.expect("initial list"); - assert!(!initial.servers.contains_key(server_name)); - - config - .add(McpConfigAddRequest { - name: server_name.to_string(), - config: json!({ "command": "node", "args": [] }), - }) - .await - .expect("add"); - let after_add = config.list().await.expect("list after add"); - assert!(after_add.servers.contains_key(server_name)); - - config - .update(McpConfigUpdateRequest { - name: server_name.to_string(), - config: json!({ "command": "node", "args": ["--version"] }), - }) - .await - .expect("update"); - let after_update = config.list().await.expect("list after update"); - let updated = after_update - .servers - .get(server_name) - .expect("updated server"); - assert_eq!( - updated.get("command").and_then(|v| v.as_str()), - Some("node") - ); - assert_eq!( - updated - .get("args") - .and_then(|v| v.as_array()) - .and_then(|args| args.first()) - .and_then(|v| v.as_str()), - Some("--version") - ); - - config - .disable(McpConfigDisableRequest { - names: vec![server_name.to_string()], - }) - .await - .expect("disable"); - config - .enable(McpConfigEnableRequest { - names: vec![server_name.to_string()], - }) - .await - .expect("enable"); - config - .remove(McpConfigRemoveRequest { - name: server_name.to_string(), - auth_client_id_metadata_url: None, - }) - .await - .expect("remove"); - - let after_remove = config.list().await.expect("list after remove"); - assert!(!after_remove.servers.contains_key(server_name)); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_round_trip_http_mcp_oauth_config_rpc() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_config", - "should_round_trip_http_mcp_oauth_config_rpc", - |ctx| { - Box::pin(async move { - let server_name = "rust-sdk-http-oauth-mcp-config"; - let client = ctx.start_client().await; - let config = client.rpc().mcp().config(); - let _ = config - .remove(McpConfigRemoveRequest { - name: server_name.to_string(), - auth_client_id_metadata_url: None, - }) - .await; - - config - .add(McpConfigAddRequest { - name: server_name.to_string(), - config: json!({ - "type": "http", - "url": "https://example.com/mcp", - "headers": { "Authorization": "Bearer token" }, - "oauthClientId": "client-id", - "oauthPublicClient": false, - "oauthGrantType": "client_credentials", - "tools": ["*"], - "timeout": 3000 - }), - }) - .await - .expect("add"); - let after_add = config.list().await.expect("list after add"); - let added = after_add.servers.get(server_name).expect("added server"); - assert_eq!(added.get("type").and_then(|v| v.as_str()), Some("http")); - assert_eq!( - added.get("url").and_then(|v| v.as_str()), - Some("https://example.com/mcp") - ); - assert_eq!( - added - .get("headers") - .and_then(|v| v.get("Authorization")) - .and_then(|v| v.as_str()), - Some("Bearer token") - ); - assert_eq!( - added.get("oauthClientId").and_then(|v| v.as_str()), - Some("client-id") - ); - assert_eq!( - added.get("oauthPublicClient").and_then(|v| v.as_bool()), - Some(false) - ); - assert_eq!( - added.get("oauthGrantType").and_then(|v| v.as_str()), - Some("client_credentials") - ); - - config - .update(McpConfigUpdateRequest { - name: server_name.to_string(), - config: json!({ - "type": "http", - "url": "https://example.com/updated-mcp", - "oauthClientId": "updated-client-id", - "oauthPublicClient": true, - "oauthGrantType": "authorization_code", - "tools": ["updated-tool"], - "timeout": 4000 - }), - }) - .await - .expect("update"); - let after_update = config.list().await.expect("list after update"); - let updated = after_update - .servers - .get(server_name) - .expect("updated server"); - assert_eq!( - updated.get("url").and_then(|v| v.as_str()), - Some("https://example.com/updated-mcp") - ); - assert_eq!( - updated.get("oauthClientId").and_then(|v| v.as_str()), - Some("updated-client-id") - ); - assert_eq!( - updated.get("oauthPublicClient").and_then(|v| v.as_bool()), - Some(true) - ); - assert_eq!( - updated.get("oauthGrantType").and_then(|v| v.as_str()), - Some("authorization_code") - ); - assert_eq!( - updated - .get("tools") - .and_then(|v| v.as_array()) - .and_then(|tools| tools.first()) - .and_then(|v| v.as_str()), - Some("updated-tool") - ); - assert_eq!(updated.get("timeout").and_then(|v| v.as_i64()), Some(4000)); - - config - .remove(McpConfigRemoveRequest { - name: server_name.to_string(), - auth_client_id_metadata_url: None, - }) - .await - .expect("remove"); - let after_remove = config.list().await.expect("list after remove"); - assert!(!after_remove.servers.contains_key(server_name)); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_mcp_config", 2); diff --git a/rust/tests/e2e/rpc_mcp_lifecycle.rs b/rust/tests/e2e/rpc_mcp_lifecycle.rs deleted file mode 100644 index 8e5b74c05b..0000000000 --- a/rust/tests/e2e/rpc_mcp_lifecycle.rs +++ /dev/null @@ -1,390 +0,0 @@ -use std::path::Path; - -use github_copilot_sdk::rpc::{ - McpConfigureGitHubResult, McpIsServerRunningRequest, McpListToolsRequest, - McpStartServersResult, McpStopServerRequest, -}; -use github_copilot_sdk::session::Session; -use github_copilot_sdk::session_events::McpServerStatus; -use github_copilot_sdk::{Error, IndexMap, McpServerConfig, McpStdioServerConfig}; -use serde::de::DeserializeOwned; -use serde_json::{Value, json}; - -use super::support::wait_for_condition; - -#[tokio::test] -async fn should_list_tools_and_report_running_status_for_connected_server() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_lifecycle", - "should_list_tools_and_report_running_status_for_connected_server", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let server_name = "rpc-lifecycle-list-server"; - let client = ctx.start_client().await; - let session = - client - .create_session(ctx.approve_all_session_config().with_mcp_servers( - create_test_mcp_servers(ctx.repo_root(), server_name), - )) - .await - .expect("create session"); - wait_for_mcp_server_status(&session, server_name, McpServerStatus::Connected).await; - - let tools = session - .rpc() - .mcp() - .list_tools(McpListToolsRequest { - server_name: server_name.to_string(), - }) - .await - .expect("list MCP tools"); - assert!(!tools.tools.is_empty()); - assert!(tools.tools.iter().all(|tool| !tool.name.trim().is_empty())); - - assert!(is_mcp_server_running(&session, server_name).await); - assert!( - !is_mcp_server_running( - &session, - &format!("missing-{}", uuid::Uuid::new_v4().simple()) - ) - .await - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_throw_when_listing_tools_for_unconnected_server() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_lifecycle", - "should_throw_when_listing_tools_for_unconnected_server", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let server_name = "rpc-lifecycle-unconnected-host"; - let client = ctx.start_client().await; - let session = - client - .create_session(ctx.approve_all_session_config().with_mcp_servers( - create_test_mcp_servers(ctx.repo_root(), server_name), - )) - .await - .expect("create session"); - wait_for_mcp_server_status(&session, server_name, McpServerStatus::Connected).await; - - let err = session - .rpc() - .mcp() - .list_tools(McpListToolsRequest { - server_name: format!("missing-{}", uuid::Uuid::new_v4().simple()), - }) - .await - .expect_err("missing server should fail"); - assert_error_contains(&err, "not connected"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_stop_running_mcp_server() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_lifecycle", - "should_stop_running_mcp_server", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let server_name = "rpc-lifecycle-stop-server"; - let client = ctx.start_client().await; - let session = - client - .create_session(ctx.approve_all_session_config().with_mcp_servers( - create_test_mcp_servers(ctx.repo_root(), server_name), - )) - .await - .expect("create session"); - wait_for_mcp_server_status(&session, server_name, McpServerStatus::Connected).await; - assert!(is_mcp_server_running(&session, server_name).await); - - session - .rpc() - .mcp() - .stop_server(McpStopServerRequest { - server_name: server_name.to_string(), - }) - .await - .expect("stop MCP server"); - - wait_for_mcp_running(&session, server_name, false).await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -// TODO(cli-1.0.81-2): CLI 1.0.81-2 no longer installs an MCP config from the inline start -// payload, so `session.mcp.startServer` reports "has no installed config to start". -// Re-enable once the runtime fix ships. -#[ignore = "blocked on CLI 1.0.81-2 MCP installed-config regression"] -#[tokio::test] -async fn should_start_and_restart_mcp_server() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_lifecycle", - "should_start_and_restart_mcp_server", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let host_server = "rpc-lifecycle-host-server"; - let client = ctx.start_client().await; - let session = - client - .create_session(ctx.approve_all_session_config().with_mcp_servers( - create_test_mcp_servers(ctx.repo_root(), host_server), - )) - .await - .expect("create session"); - wait_for_mcp_server_status(&session, host_server, McpServerStatus::Connected).await; - - let started_server = "rpc-lifecycle-started-server"; - let config = test_mcp_server_config(ctx.repo_root()); - let config_value = serde_json::to_value(&config).expect("serialize MCP config"); - call_session_rpc( - &session, - "session.mcp.startServer", - json!({ "serverName": started_server, "config": config_value }), - ) - .await - .expect("start MCP server"); - wait_for_mcp_running(&session, started_server, true).await; - - let tools = session - .rpc() - .mcp() - .list_tools(McpListToolsRequest { - server_name: started_server.to_string(), - }) - .await - .expect("list started MCP tools"); - assert!(!tools.tools.is_empty()); - - let config_value = serde_json::to_value(&config).expect("serialize MCP config"); - call_session_rpc( - &session, - "session.mcp.restartServer", - json!({ "serverName": started_server, "config": config_value }), - ) - .await - .expect("restart MCP server"); - wait_for_mcp_running(&session, started_server, true).await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -// There is deliberately no e2e test for `session.mcp.registerExternalClient`. That method is -// marked `visibility: internal` in the shared API contract: its `client` and `transport` fields -// are live in-process MCP SDK instances, so it cannot be driven over JSON-RPC, and no SDK -// exposes it as a typed method. A raw-RPC test used to pass only because older CLIs routed -// internal methods generically; it never exercised a supported wire API. - -#[tokio::test] -#[ignore = "blocked on CLI 1.0.81-6 missing session.mcp.reloadWithConfig handler"] -async fn should_reload_mcp_servers_with_config() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_lifecycle", - "should_reload_mcp_servers_with_config", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let host_server = "rpc-lifecycle-reload-host"; - let client = ctx.start_client().await; - let session = - client - .create_session(ctx.approve_all_session_config().with_mcp_servers( - create_test_mcp_servers(ctx.repo_root(), host_server), - )) - .await - .expect("create session"); - wait_for_mcp_server_status(&session, host_server, McpServerStatus::Connected).await; - - let result: McpStartServersResult = call_session_rpc_typed( - &session, - "session.mcp.reloadWithConfig", - json!({ - "config": { - "mcpServers": {}, - "disabledServers": [] - } - }), - ) - .await - .expect("reload MCP with config"); - - assert!(result.filtered_servers.is_empty()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -#[ignore = "blocked on CLI 1.0.81-6 missing session.mcp.configureGitHub handler"] -async fn should_configure_github_mcp_server() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_mcp_lifecycle", - "should_configure_github_mcp_server", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let host_server = "rpc-lifecycle-configure-host"; - let client = ctx.start_client().await; - let session = - client - .create_session(ctx.approve_all_session_config().with_mcp_servers( - create_test_mcp_servers(ctx.repo_root(), host_server), - )) - .await - .expect("create session"); - wait_for_mcp_server_status(&session, host_server, McpServerStatus::Connected).await; - - let result: McpConfigureGitHubResult = call_session_rpc_typed( - &session, - "session.mcp.configureGitHub", - json!({ "authInfo": { "type": "api-key" } }), - ) - .await - .expect("configure GitHub MCP"); - - assert!(!result.changed); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -fn create_test_mcp_servers( - repo_root: &Path, - server_name: &str, -) -> IndexMap { - IndexMap::from([(server_name.to_string(), test_mcp_server_config(repo_root))]) -} - -fn test_mcp_server_config(repo_root: &Path) -> McpServerConfig { - let harness_dir = repo_root.join("test").join("harness"); - let server_path = harness_dir - .join("test-mcp-server.mjs") - .to_string_lossy() - .to_string(); - McpServerConfig::Stdio(McpStdioServerConfig { - tools: Some(vec!["*".to_string()]), - command: if cfg!(windows) { - "node.exe".to_string() - } else { - "node".to_string() - }, - args: vec![server_path], - working_directory: Some(harness_dir.to_string_lossy().to_string()), - ..McpStdioServerConfig::default() - }) -} - -async fn wait_for_mcp_server_status( - session: &Session, - server_name: &str, - expected_status: McpServerStatus, -) { - wait_for_condition("MCP server status", || async { - session - .rpc() - .mcp() - .list() - .await - .expect("list MCP servers") - .servers - .iter() - .any(|server| server.name == server_name && server.status == expected_status) - }) - .await; -} - -async fn wait_for_mcp_running(session: &Session, server_name: &str, expected_running: bool) { - wait_for_condition("MCP server running state", || async { - is_mcp_server_running(session, server_name).await == expected_running - }) - .await; -} - -async fn is_mcp_server_running(session: &Session, server_name: &str) -> bool { - session - .rpc() - .mcp() - .is_server_running(McpIsServerRunningRequest { - server_name: server_name.to_string(), - }) - .await - .expect("check MCP running") - .running -} - -async fn call_session_rpc( - session: &Session, - method: &'static str, - mut params: Value, -) -> Result { - params["sessionId"] = json!(session.id()); - session.client().call(method, Some(params)).await -} - -async fn call_session_rpc_typed( - session: &Session, - method: &'static str, - params: Value, -) -> Result { - let value = call_session_rpc(session, method, params).await?; - Ok(serde_json::from_value(value)?) -} - -fn assert_error_contains(err: &Error, expected: &str) { - let message = err.to_string(); - assert!( - !message.to_ascii_lowercase().contains("unhandled method"), - "{message}" - ); - assert!( - message - .to_ascii_lowercase() - .contains(&expected.to_ascii_lowercase()), - "expected error to contain {expected:?}, got {message}" - ); -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_mcp_lifecycle", 6); diff --git a/rust/tests/e2e/rpc_queue.rs b/rust/tests/e2e/rpc_queue.rs deleted file mode 100644 index 02f9399947..0000000000 --- a/rust/tests/e2e/rpc_queue.rs +++ /dev/null @@ -1,242 +0,0 @@ -use github_copilot_sdk::rpc::{ - CommandsRespondToQueuedCommandRequest, EnqueueCommandParams, EnqueueCommandResult, - QueuePendingItems, QueuePendingItemsKind, RegisterEventInterestParams, - ReleaseEventInterestParams, -}; -use github_copilot_sdk::session::Session; -use github_copilot_sdk::session_events::{CommandQueuedData, SessionEventType}; -use serde_json::json; -use uuid::Uuid; - -use super::support::{wait_for_condition, wait_for_event}; - -fn is_pending_command(item: &QueuePendingItems, command: &str) -> bool { - item.kind == QueuePendingItemsKind::Command - && (item.display_text == command - || item.display_text.contains(command.trim_start_matches('/'))) -} - -async fn wait_for_command_in_pending_items(session: &Session, command: &str) { - wait_for_condition( - "queued command to appear in pending items", - move || async move { - session - .rpc() - .queue() - .pending_items() - .await - .expect("pending queued command") - .items - .iter() - .any(|item| is_pending_command(item, command)) - }, - ) - .await; -} - -async fn wait_for_command_not_in_pending_items(session: &Session, command: &str) { - wait_for_condition( - "queued command to leave pending items", - move || async move { - !session - .rpc() - .queue() - .pending_items() - .await - .expect("pending queued command") - .items - .iter() - .any(|item| is_pending_command(item, command)) - }, - ) - .await; -} - -async fn wait_for_queue_empty(session: &Session) { - wait_for_condition("queue to empty", move || async move { - let pending = session - .rpc() - .queue() - .pending_items() - .await - .expect("pending after clear"); - pending.items.is_empty() && pending.steering_messages.is_empty() - }) - .await; -} - -#[tokio::test] -async fn fresh_queue_is_empty_and_empty_mutations_are_noops() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_queue", - "fresh_queue_is_empty_and_empty_mutations_are_noops", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let pending = session - .rpc() - .queue() - .pending_items() - .await - .expect("pending items"); - assert!(pending.items.is_empty()); - assert!(pending.steering_messages.is_empty()); - assert!( - !session - .rpc() - .queue() - .remove_most_recent() - .await - .expect("remove most recent") - .removed - ); - session.rpc().queue().clear().await.expect("clear queue"); - let after = session - .rpc() - .queue() - .pending_items() - .await - .expect("pending after clear"); - assert!(after.items.is_empty()); - assert!(after.steering_messages.is_empty()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn pendingitems_reports_queued_command_and_remove_and_clear_update_queue() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_queue", - "pendingitems_reports_queued_command_and_remove_and_clear_update_queue", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let first_command = format!("/sdk-queue-first-{}", Uuid::new_v4()); - let second_command = format!("/sdk-queue-second-{}", Uuid::new_v4()); - let third_command = format!("/sdk-queue-third-{}", Uuid::new_v4()); - let interest = session - .rpc() - .event_log() - .register_interest(RegisterEventInterestParams { - event_type: "command.queued".to_string(), - }) - .await - .expect("register command interest") - .handle; - let first_command_for_event = first_command.clone(); - let queued_event = - wait_for_event(session.subscribe(), "command queued", move |event| { - event.parsed_type() == SessionEventType::CommandQueued - && event.data.get("command").and_then(|value| value.as_str()) - == Some(first_command_for_event.as_str()) - }); - - let enqueue = session - .rpc() - .commands() - .enqueue(EnqueueCommandParams { - command: first_command, - display_text: None, - }) - .await - .expect("enqueue command"); - assert!(matches!( - enqueue, - EnqueueCommandResult::AcceptedEnqueueCommandResult(result) if result.queued - )); - let queued = queued_event - .await - .typed_data::() - .expect("command queued data"); - - let second = session - .rpc() - .commands() - .enqueue(EnqueueCommandParams { - command: second_command.clone(), - display_text: None, - }) - .await - .expect("enqueue second command"); - assert!(matches!( - second, - EnqueueCommandResult::AcceptedEnqueueCommandResult(result) if result.queued - )); - wait_for_command_in_pending_items(&session, &second_command).await; - - let removed = session - .rpc() - .queue() - .remove_most_recent() - .await - .expect("remove second command"); - assert!(removed.removed); - wait_for_command_not_in_pending_items(&session, &second_command).await; - - let third = session - .rpc() - .commands() - .enqueue(EnqueueCommandParams { - command: third_command.clone(), - display_text: None, - }) - .await - .expect("enqueue third command"); - assert!(matches!( - third, - EnqueueCommandResult::AcceptedEnqueueCommandResult(result) if result.queued - )); - wait_for_command_in_pending_items(&session, &third_command).await; - - session.rpc().queue().clear().await.expect("clear queue"); - wait_for_command_not_in_pending_items(&session, &third_command).await; - - let completed = session - .rpc() - .commands() - .respond_to_queued_command(CommandsRespondToQueuedCommandRequest { - request_id: queued.request_id, - result: json!({ - "handled": true, - "stopProcessingQueue": true - }), - }) - .await - .expect("respond to first command"); - assert!(completed.success); - - wait_for_queue_empty(&session).await; - session - .rpc() - .event_log() - .release_interest(ReleaseEventInterestParams { handle: interest }) - .await - .expect("release command interest"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_queue", 2); diff --git a/rust/tests/e2e/rpc_remote.rs b/rust/tests/e2e/rpc_remote.rs deleted file mode 100644 index e98f6c4faa..0000000000 --- a/rust/tests/e2e/rpc_remote.rs +++ /dev/null @@ -1,119 +0,0 @@ -use github_copilot_sdk::rpc::{RemoteEnableRequest, RemoteSessionMode}; -use github_copilot_sdk::session_events::{SessionEventType, SessionRemoteSteerableChangedData}; - -use super::support::wait_for_event; - -#[tokio::test] -async fn should_treat_remote_off_as_noop_or_implemented_error() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_remote", - "should_treat_remote_off_as_noop_or_implemented_error", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - match session - .rpc() - .remote() - .enable(RemoteEnableRequest { - mode: Some(RemoteSessionMode::Off), - }) - .await - { - Ok(result) => { - assert!(!result.remote_steerable); - assert!(result.url.as_deref().unwrap_or_default().is_empty()); - } - Err(err) => assert!( - !err.to_string() - .contains("Unhandled method session.remote.enable") - ), - } - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_treat_remote_disable_as_noop_or_implemented_error() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_remote", - "should_treat_remote_disable_as_noop_or_implemented_error", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - if let Err(err) = session.rpc().remote().disable().await { - assert!( - !err.to_string() - .contains("Unhandled method session.remote.disable") - ); - } - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_notify_steerable_changed_event_and_persist_flag() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_remote", - "should_notify_steerable_changed_event_and_persist_flag", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let changed = - wait_for_event(session.subscribe(), "remote steerable changed", |event| { - event.parsed_type() == SessionEventType::SessionRemoteSteerableChanged - && event - .typed_data::() - .is_some_and(|data| data.remote_steerable) - }); - - session - .rpc() - .remote() - .notify_steerable_changed( - github_copilot_sdk::rpc::RemoteNotifySteerableChangedRequest { - remote_steerable: true, - }, - ) - .await - .expect("notify remote steerable"); - changed.await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_remote", 3); diff --git a/rust/tests/e2e/rpc_schedule.rs b/rust/tests/e2e/rpc_schedule.rs deleted file mode 100644 index af8f6f59b7..0000000000 --- a/rust/tests/e2e/rpc_schedule.rs +++ /dev/null @@ -1,75 +0,0 @@ -use github_copilot_sdk::rpc::ScheduleStopRequest; - -#[tokio::test] -async fn should_list_no_schedules_for_fresh_session() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_schedule", - "should_list_no_schedules_for_fresh_session", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let schedules = session - .rpc() - .schedule() - .list() - .await - .expect("list schedules"); - assert!(schedules.entries.is_empty()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_return_null_entry_when_stopping_unknown_schedule() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_schedule", - "should_return_null_entry_when_stopping_unknown_schedule", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let stopped = session - .rpc() - .schedule() - .stop(ScheduleStopRequest { id: i64::MAX }) - .await - .expect("stop missing schedule"); - assert!(stopped.entry.is_none()); - assert!( - session - .rpc() - .schedule() - .list() - .await - .expect("list schedules") - .entries - .is_empty() - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_schedule", 2); diff --git a/rust/tests/e2e/rpc_server.rs b/rust/tests/e2e/rpc_server.rs deleted file mode 100644 index d6bac1b43a..0000000000 --- a/rust/tests/e2e/rpc_server.rs +++ /dev/null @@ -1,924 +0,0 @@ -use std::collections::HashMap; - -use github_copilot_sdk::rpc::{ - AgentsDiscoverRequest, AgentsGetDiscoveryPathsRequest, ConnectRemoteSessionParams, - InstructionsDiscoverRequest, InstructionsGetDiscoveryPathsRequest, - LlmInferenceHttpResponseChunkRequest, LlmInferenceHttpResponseStartRequest, - LocalSessionMetadataValue, McpDiscoverRequest, NameSetRequest, PingRequest, - SecretsAddFilterValuesRequest, SessionContext, SessionFsSetProviderConventions, - SessionFsSetProviderRequest, SessionListFilter, SessionsBulkDeleteRequest, - SessionsCheckInUseRequest, SessionsCloseRequest, SessionsEnrichMetadataRequest, - SessionsFindByPrefixRequest, SessionsFindByTaskIDRequest, SessionsGetLastForContextRequest, - SessionsListRequest, SessionsLoadDeferredRepoHooksRequest, SessionsPruneOldRequest, - SessionsReleaseLockRequest, SessionsReloadPluginHooksRequest, SessionsSaveRequest, - SessionsSetAdditionalPluginsRequest, SkillsConfigSetDisabledSkillsRequest, - SkillsDiscoverRequest, SkillsGetDiscoveryPathsRequest, ToolsListRequest, -}; -use github_copilot_sdk::{Client, RequestId}; -use serde_json::json; - -use super::support::{with_e2e_context, with_e2e_context_no_snapshot}; - -#[tokio::test] -async fn should_call_rpc_ping_with_typed_params_and_result() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server", - "should_call_rpc_ping_with_typed_params_and_result", - |ctx| { - Box::pin(async move { - let client = ctx.start_client().await; - - let result = client - .rpc() - .ping(PingRequest { - message: Some("typed rpc test".to_string()), - }) - .await - .expect("ping"); - - assert_eq!(result.message, "pong: typed rpc test"); - assert!(!result.timestamp.is_empty()); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_clear_the_managed_settings_cache() { - if super::support::skip_inprocess("managedSettings.clearCache is unavailable in-process") { - return; - } - with_e2e_context( - "rpc_server", - "should_clear_the_managed_settings_cache", - |ctx| { - Box::pin(async move { - let client = ctx.start_client().await; - - client - .rpc() - .managed_settings() - .clear_cache() - .await - .expect("clear managed settings cache"); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_call_rpc_models_list_with_typed_result() { - // TODO(cli-1.0.81-2): CLI 1.0.81-2 stopped honoring client-level GitHub tokens over the - // in-process (FFI) host, which resolves auth from the ambient environment instead. - if super::support::skip_inprocess("client-level GitHub tokens are not supported in-process") { - return; - } - with_e2e_context( - "rpc_server", - "should_call_rpc_models_list_with_typed_result", - |ctx| { - Box::pin(async move { - let token = "rpc-models-token"; - ctx.set_copilot_user_by_token_with_login(token, "rpc-user"); - let client = Client::start(ctx.client_options_with_github_token(token)) - .await - .expect("start client"); - - let result = client.rpc().models().list().await.expect("models list"); - - assert!( - result - .models - .iter() - .any(|model| model.id == "claude-sonnet-5") - ); - assert!(result.models.iter().all(|model| !model.name.is_empty())); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_call_rpc_account_get_quota_when_authenticated() { - // TODO(cli-1.0.81-2): CLI 1.0.81-2 stopped honoring client-level GitHub tokens over the - // in-process (FFI) host, which resolves auth from the ambient environment instead. - if super::support::skip_inprocess("client-level GitHub tokens are not supported in-process") { - return; - } - with_e2e_context( - "rpc_server", - "should_call_rpc_account_get_quota_when_authenticated", - |ctx| { - Box::pin(async move { - let token = "rpc-quota-token"; - ctx.set_copilot_user_by_token_with_login_and_quota( - token, - "rpc-user", - Some(json!({ - "chat": { - "entitlement": 100, - "overage_count": 2, - "overage_permitted": true, - "percent_remaining": 75, - "timestamp_utc": "2026-04-30T00:00:00Z" - } - })), - ); - let client = Client::start(ctx.client_options_with_github_token(token)) - .await - .expect("start client"); - - let result = client.rpc().account().get_quota().await.expect("quota"); - let chat = result.quota_snapshots.get("chat").expect("chat quota"); - - assert_eq!(chat.entitlement_requests, 100); - assert_eq!(chat.used_requests, 25); - assert_eq!(chat.remaining_percentage, 75.0); - assert_eq!(chat.overage, 2.0); - assert!(chat.usage_allowed_with_exhausted_quota); - assert!(chat.overage_allowed_with_exhausted_quota); - assert_eq!(chat.reset_date.as_deref(), Some("2026-04-30T00:00:00Z")); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_call_rpc_tools_list_with_typed_result() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server", - "should_call_rpc_tools_list_with_typed_result", - |ctx| { - Box::pin(async move { - let client = ctx.start_client().await; - - let result = client - .rpc() - .tools() - .list(ToolsListRequest { model: None }) - .await - .expect("tools list"); - - assert!(!result.tools.is_empty()); - assert!(result.tools.iter().all(|tool| !tool.name.is_empty())); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_reject_llm_response_frames_for_unknown_request() { - with_e2e_context_no_snapshot(|ctx| { - Box::pin(async move { - let client = ctx.start_client().await; - let request_id = RequestId::from("missing-llm-response-request"); - - let start = client - .rpc() - .llm_inference() - .http_response_start(LlmInferenceHttpResponseStartRequest { - headers: HashMap::from([( - "content-type".to_string(), - vec!["application/json".to_string()], - )]), - request_id: request_id.clone(), - status: 200, - status_text: Some("OK".to_string()), - }) - .await - .expect("send unknown LLM response start"); - assert!(!start.accepted); - - let chunk = client - .rpc() - .llm_inference() - .http_response_chunk(LlmInferenceHttpResponseChunkRequest { - binary: Some(false), - data: "{}".to_string(), - end: Some(true), - error: None, - request_id, - }) - .await - .expect("send unknown LLM response chunk"); - assert!(!chunk.accepted); - - client.stop().await.expect("stop client"); - }) - }) - .await; -} - -#[tokio::test] -async fn should_discover_server_mcp_and_skills() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server", - "should_discover_server_mcp_and_skills", - |ctx| { - Box::pin(async move { - let skill_name = "server-rpc-skill-rust"; - let skill_directory = create_skill_directory( - ctx.work_dir(), - skill_name, - "Skill discovered by server-scoped RPC tests.", - ); - let client = ctx.start_client().await; - let project_path = ctx.work_dir().to_string_lossy().to_string(); - - let mcp = client - .rpc() - .mcp() - .discover(McpDiscoverRequest { - include_effective_source: None, - working_directory: Some(project_path.clone()), - }) - .await - .expect("mcp discover"); - assert!(mcp.servers.iter().all(|server| !server.name.is_empty())); - - let skills = client - .rpc() - .skills() - .discover(SkillsDiscoverRequest { - exclude_host_skills: None, - ignored_skills_locations: None, - project_paths: None, - skill_directories: Some(vec![ - skill_directory.to_string_lossy().to_string(), - ]), - }) - .await - .expect("skills discover"); - let discovered = assert_server_skill(skills, skill_name, true); - assert_eq!( - discovered.description, - "Skill discovered by server-scoped RPC tests." - ); - - let skill_paths = client - .rpc() - .skills() - .get_discovery_paths(SkillsGetDiscoveryPathsRequest { - exclude_host_skills: Some(true), - ignored_skills_locations: None, - project_paths: Some(vec![project_path.clone()]), - }) - .await - .expect("skills discovery paths"); - let project_skill_path = skill_paths - .paths - .iter() - .find(|path| { - path.project_path - .as_deref() - .is_some_and(|path| paths_equal(path, &project_path)) - && path.preferred_for_creation - }) - .expect("project skill discovery path"); - assert!(!project_skill_path.path.trim().is_empty()); - - let agents = client - .rpc() - .agents() - .discover(AgentsDiscoverRequest { - exclude_host_agents: Some(true), - project_paths: Some(vec![project_path.clone()]), - }) - .await - .expect("agents discover"); - assert!( - agents - .agents - .iter() - .all(|agent| !agent.name.trim().is_empty()) - ); - - let agent_paths = client - .rpc() - .agents() - .get_discovery_paths(AgentsGetDiscoveryPathsRequest { - exclude_host_agents: Some(true), - project_paths: Some(vec![project_path.clone()]), - }) - .await - .expect("agents discovery paths"); - let project_agent_path = agent_paths - .paths - .iter() - .find(|path| { - path.project_path - .as_deref() - .is_some_and(|path| paths_equal(path, &project_path)) - && path.preferred_for_creation - }) - .expect("project agent discovery path"); - assert!(!project_agent_path.path.trim().is_empty()); - - let instructions = client - .rpc() - .instructions() - .discover(InstructionsDiscoverRequest { - exclude_host_instructions: Some(true), - project_paths: Some(vec![project_path.clone()]), - }) - .await - .expect("instructions discover"); - assert!(instructions.sources.iter().all(|source| { - !source.id.trim().is_empty() - && !source.label.trim().is_empty() - && !source.source_path.trim().is_empty() - })); - - let instruction_paths = client - .rpc() - .instructions() - .get_discovery_paths(InstructionsGetDiscoveryPathsRequest { - exclude_host_instructions: Some(true), - project_paths: Some(vec![project_path.clone()]), - }) - .await - .expect("instructions discovery paths"); - assert!(!instruction_paths.paths.is_empty()); - assert!(instruction_paths.paths.iter().any(|path| { - path.project_path - .as_deref() - .is_some_and(|path| paths_equal(path, &project_path)) - })); - assert!( - instruction_paths - .paths - .iter() - .all(|path| !path.path.trim().is_empty()) - ); - - client - .rpc() - .skills() - .config() - .set_disabled_skills(SkillsConfigSetDisabledSkillsRequest { - disabled_skills: vec![skill_name.to_string()], - }) - .await - .expect("disable skill globally"); - let disabled_skills = client - .rpc() - .skills() - .discover(SkillsDiscoverRequest { - exclude_host_skills: None, - ignored_skills_locations: None, - project_paths: None, - skill_directories: Some(vec![ - skill_directory.to_string_lossy().to_string(), - ]), - }) - .await - .expect("skills discover disabled"); - assert_server_skill(disabled_skills, skill_name, false); - - client - .rpc() - .skills() - .config() - .set_disabled_skills(SkillsConfigSetDisabledSkillsRequest { - disabled_skills: Vec::new(), - }) - .await - .expect("clear disabled skills"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_call_rpc_sessionfs_setprovider_with_typed_result() { - with_e2e_context( - "rpc_server", - "should_call_rpc_sessionfs_setprovider_with_typed_result", - |ctx| { - Box::pin(async move { - let client = ctx.start_client().await; - - let result = client - .rpc() - .session_fs() - .set_provider(SessionFsSetProviderRequest { - capabilities: None, - conventions: if cfg!(windows) { - SessionFsSetProviderConventions::Windows - } else { - SessionFsSetProviderConventions::Posix - }, - initial_cwd: ctx.work_dir().display().to_string(), - session_state_path: ctx - .work_dir() - .join("session-state") - .display() - .to_string(), - }) - .await - .expect("set session fs provider"); - assert!(result.success); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_add_secret_filter_values() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server", - "should_add_secret_filter_values", - |ctx| { - Box::pin(async move { - let client = ctx.start_client().await; - - let result = client - .rpc() - .secrets() - .add_filter_values(SecretsAddFilterValuesRequest { - values: vec!["rust-secret-value".to_string()], - }) - .await; - match result { - Ok(response) => assert!(response.ok), - Err(err) => { - let message = err.to_string(); - assert!(message.contains("COPILOT_ENABLE_SECRET_FILTERING")); - assert!(!message.contains("Unhandled method secrets.addFilterValues")); - } - } - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_list_find_and_inspect_persisted_session_state() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server", - "should_list_find_and_inspect_persisted_session_state", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - session - .rpc() - .name() - .set(NameSetRequest { - name: "Rust persisted session".to_string(), - }) - .await - .expect("set session name"); - let session_id = session.id().clone(); - client - .rpc() - .sessions() - .save(SessionsSaveRequest { - session_id: session_id.clone(), - }) - .await - .expect("save session"); - session.disconnect().await.expect("disconnect session"); - - let list = client.rpc().sessions().list().await.expect("list sessions"); - assert!(list.sessions.iter().all(|metadata| { - metadata - .get("sessionId") - .and_then(serde_json::Value::as_str) - .is_some_and(|id| !id.is_empty()) - })); - let filtered = client - .rpc() - .sessions() - .list_with_params(SessionsListRequest { - filter: Some(SessionListFilter { - cwd: Some(ctx.work_dir().display().to_string()), - branch: None, - git_root: None, - repository: None, - }), - include_detached: None, - metadata_limit: Some(10), - source: None, - throw_on_error: None, - }) - .await - .expect("filtered sessions"); - assert!(filtered.sessions.iter().all(|metadata| { - metadata - .get("context") - .and_then(|context| context.get("cwd")) - .and_then(serde_json::Value::as_str) - .is_none_or(|cwd| cwd == ctx.work_dir().display().to_string()) - })); - assert!( - client - .rpc() - .sessions() - .find_by_prefix(SessionsFindByPrefixRequest { - prefix: "0000000".to_string(), - }) - .await - .expect("find missing prefix") - .session_id - .is_none() - ); - assert!( - client - .rpc() - .sessions() - .find_by_task_id(SessionsFindByTaskIDRequest { - task_id: "missing-rust-task".to_string(), - }) - .await - .expect("find by task id") - .session_id - .is_none() - ); - client - .rpc() - .sessions() - .get_last_for_context(SessionsGetLastForContextRequest { context: None }) - .await - .expect("last for context"); - assert!( - client - .rpc() - .sessions() - .get_sizes() - .await - .expect("session sizes") - .sizes - .values() - .all(|size| *size >= 0) - ); - let in_use = client - .rpc() - .sessions() - .check_in_use(SessionsCheckInUseRequest { - session_ids: vec![session_id.to_string(), "missing-session-id".to_string()], - }) - .await - .expect("check in use"); - assert!(!in_use.in_use.iter().any(|id| id == "missing-session-id")); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_enrich_basic_session_metadata() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server", - "should_enrich_basic_session_metadata", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let session_id = session.id().clone(); - let metadata = LocalSessionMetadataValue { - client_name: None, - context: Some(SessionContext { - branch: None, - cwd: ctx.work_dir().display().to_string(), - git_root: None, - host_type: None, - repository: None, - }), - is_detached: None, - is_remote: false, - mc_task_id: None, - modified_time: "2026-01-01T00:00:00.000Z".to_string(), - name: Some("Rust metadata".to_string()), - session_id: session_id.clone(), - start_time: "2026-01-01T00:00:00.000Z".to_string(), - summary: None, - }; - - let enriched = client - .rpc() - .sessions() - .enrich_metadata(SessionsEnrichMetadataRequest { - sessions: vec![metadata], - }) - .await - .expect("enrich metadata"); - let enriched = enriched.sessions.first().expect("enriched session"); - assert_eq!(enriched.session_id, session_id); - assert!(!enriched.is_remote); - assert!(enriched.context.is_some()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_close_active_session_and_release_lock() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server", - "should_close_active_session_and_release_lock", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let session_id = session.id().clone(); - - client - .rpc() - .sessions() - .close(SessionsCloseRequest { - session_id: session_id.clone(), - }) - .await - .expect("close session"); - client - .rpc() - .sessions() - .release_lock(SessionsReleaseLockRequest { - session_id: session_id.clone(), - }) - .await - .expect("release lock"); - assert!( - !client - .rpc() - .sessions() - .check_in_use(SessionsCheckInUseRequest { - session_ids: vec![session_id.to_string()], - }) - .await - .expect("check after release") - .in_use - .contains(&session_id.to_string()) - ); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_prune_dryrun_and_bulkdelete_persisted_session() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server", - "should_prune_dryrun_and_bulkdelete_persisted_session", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let session_id = session.id().clone(); - session.disconnect().await.expect("disconnect session"); - - let prune = client - .rpc() - .sessions() - .prune_old(SessionsPruneOldRequest { - older_than_days: 0, - dry_run: Some(true), - include_named: Some(true), - exclude_session_ids: Some(vec![session_id.to_string()]), - }) - .await - .expect("dry-run prune"); - assert!(prune.dry_run); - assert!(prune.deleted.is_empty()); - assert!(!prune.candidates.iter().any(|id| id == session_id.as_str())); - let deleted = client - .rpc() - .sessions() - .bulk_delete(SessionsBulkDeleteRequest { - session_ids: vec![session_id.to_string()], - }) - .await - .expect("bulk delete"); - assert!(deleted.freed_bytes.contains_key(session_id.as_str())); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_set_additional_plugins_and_reload_deferred_hooks() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server", - "should_set_additional_plugins_and_reload_deferred_hooks", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - client - .rpc() - .sessions() - .set_additional_plugins(SessionsSetAdditionalPluginsRequest { - plugins: Vec::new(), - }) - .await - .expect("set additional plugins"); - client - .rpc() - .sessions() - .reload_plugin_hooks(SessionsReloadPluginHooksRequest { - session_id: session.id().clone(), - defer_repo_hooks: Some(true), - }) - .await - .expect("reload plugin hooks"); - let loaded = client - .rpc() - .sessions() - .load_deferred_repo_hooks(SessionsLoadDeferredRepoHooksRequest { - session_id: session.id().clone(), - }) - .await - .expect("load deferred hooks"); - assert!(loaded.startup_prompts.is_empty()); - assert_eq!(loaded.hook_count, 0); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_save_and_get_event_file_path() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server", - "should_save_and_get_event_file_path", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - client - .rpc() - .sessions() - .save(SessionsSaveRequest { - session_id: session.id().clone(), - }) - .await - .expect("save session"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_implemented_error_when_connecting_unknown_remote_session() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server", - "should_report_implemented_error_when_connecting_unknown_remote_session", - |ctx| { - Box::pin(async move { - let client = ctx.start_client().await; - - let err = client - .rpc() - .sessions() - .connect(ConnectRemoteSessionParams { - session_id: github_copilot_sdk::SessionId::from( - "00000000-0000-0000-0000-000000000000", - ), - }) - .await - .expect_err("unknown remote session should fail"); - assert!( - !err.to_string() - .contains("Unhandled method sessions.connect") - ); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -fn create_skill_directory( - work_dir: &std::path::Path, - skill_name: &str, - description: &str, -) -> std::path::PathBuf { - let skills_dir = work_dir.join("server-rpc-skills"); - let skill_dir = skills_dir.join(skill_name); - std::fs::create_dir_all(&skill_dir).expect("create skill dir"); - std::fs::write( - skill_dir.join("SKILL.md"), - format!( - "---\nname: {skill_name}\ndescription: {description}\n---\n\n# {skill_name}\n\nThis skill is used by RPC E2E tests.\n" - ), - ) - .expect("write skill"); - skills_dir -} - -fn assert_server_skill( - list: github_copilot_sdk::rpc::ServerSkillList, - skill_name: &str, - enabled: bool, -) -> github_copilot_sdk::rpc::ServerSkill { - let skill = list - .skills - .into_iter() - .find(|skill| skill.name == skill_name) - .unwrap_or_else(|| panic!("skill {skill_name} not found")); - assert_eq!(skill.enabled, enabled); - assert!( - skill - .path - .as_deref() - .is_some_and(|path| path.contains(skill_name) && path.ends_with("SKILL.md")) - ); - skill -} - -fn paths_equal(left: &str, right: &str) -> bool { - fn normalize(path: &str) -> String { - let mut normalized = path.replace('\\', "/"); - while normalized.ends_with('/') && normalized.len() > 1 { - normalized.pop(); - } - if cfg!(windows) { - normalized.to_ascii_lowercase() - } else { - normalized - } - } - - normalize(left) == normalize(right) -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_server", 11); diff --git a/rust/tests/e2e/rpc_server_misc.rs b/rust/tests/e2e/rpc_server_misc.rs deleted file mode 100644 index e53cce2e59..0000000000 --- a/rust/tests/e2e/rpc_server_misc.rs +++ /dev/null @@ -1,374 +0,0 @@ -use github_copilot_sdk::Client; -use github_copilot_sdk::rpc::{ - AccountLoginRequest, AccountLogoutRequest, AgentRegistrySpawnRequest, AuthInfo, - SendAttachmentsToMessageParams, SessionsOpenStatus, UserSettingsSetRequest, -}; -use serde_json::{Map, Value, json}; - -use super::support::{wait_for_condition, with_e2e_context}; - -#[tokio::test] -async fn should_reload_user_settings() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server_misc", - "should_reload_user_settings", - |ctx| { - Box::pin(async move { - let client = ctx.start_client().await; - - client - .rpc() - .user() - .settings() - .reload() - .await - .expect("reload user settings"); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_get_set_and_clear_user_settings() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server_misc", - "should_get_set_and_clear_user_settings", - |ctx| { - Box::pin(async move { - let client = ctx.start_client().await; - - let initial = client - .rpc() - .user() - .settings() - .get() - .await - .expect("get initial user settings"); - let (key, value) = initial - .settings - .iter() - .find_map(|(key, setting)| { - setting.value.as_bool().map(|value| (key.clone(), value)) - }) - .expect("at least one boolean user setting"); - let toggled = !value; - - let set = client - .rpc() - .user() - .settings() - .set(UserSettingsSetRequest { - settings: setting_patch(&key, json!(toggled)), - }) - .await - .expect("set user setting"); - assert!(set.shadowed_keys.is_empty()); - client - .rpc() - .user() - .settings() - .reload() - .await - .expect("reload after set"); - let after_set = client - .rpc() - .user() - .settings() - .get() - .await - .expect("get after set"); - let metadata = after_set.settings.get(&key).expect("updated setting"); - assert_eq!(metadata.value, json!(toggled)); - assert!(!metadata.is_default); - - let clear = client - .rpc() - .user() - .settings() - .set(UserSettingsSetRequest { - settings: setting_patch(&key, Value::Null), - }) - .await - .expect("clear user setting"); - assert!(clear.shadowed_keys.is_empty()); - client - .rpc() - .user() - .settings() - .reload() - .await - .expect("reload after clear"); - let after_clear = client - .rpc() - .user() - .settings() - .get() - .await - .expect("get after clear"); - assert!( - after_clear - .settings - .get(&key) - .expect("cleared setting") - .is_default - ); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_login_list_getcurrentauth_and_logout_account() { - with_e2e_context( - "rpc_server_misc", - "should_login_list_getcurrentauth_and_logout_account", - |ctx| { - Box::pin(async move { - ctx.set_copilot_user_by_token_with_login("rust-account-token", "rust-account-user"); - let client = Client::start(ctx.client_options().with_use_logged_in_user(false)) - .await - .expect("start no-token client"); - - let initial = client - .rpc() - .account() - .get_current_auth() - .await - .expect("get initial auth"); - assert!(initial.auth_info.is_none()); - - let login = client - .rpc() - .account() - .login(AccountLoginRequest { - host: "https://github.com".to_string(), - login: Some("rust-account-user".to_string()), - token: "rust-account-token".to_string(), - }) - .await - .expect("account login"); - let _stored_in_vault = login.stored_in_vault; - - let current = client - .rpc() - .account() - .get_current_auth() - .await - .expect("get current auth after login"); - let auth_info = current.auth_info.expect("auth info after login"); - let AuthInfo::User(user_auth_info) = &auth_info else { - panic!("expected user auth info, got {auth_info:?}"); - }; - assert_eq!(user_auth_info.login, "rust-account-user"); - assert_eq!(user_auth_info.host, "https://github.com"); - - let users = client - .rpc() - .account() - .get_all_users() - .await - .expect("get all users"); - if let Some(user) = users.iter().find(|user| { - matches!( - &user.auth_info, - AuthInfo::User(auth_info) if auth_info.login == "rust-account-user" - ) - }) { - user.token - .as_deref() - .filter(|token| *token == "rust-account-token") - .unwrap_or_else(|| { - panic!("expected stored account token, got {:?}", user.token) - }); - } - - let logout = client - .rpc() - .account() - .logout(AccountLogoutRequest { - auth_info: Some(auth_info), - ..Default::default() - }) - .await - .expect("account logout"); - assert!(!logout.has_more_users); - assert!( - client - .rpc() - .account() - .get_current_auth() - .await - .expect("get auth after logout") - .auth_info - .is_none() - ); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_agent_registry_spawn_gate_closed() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server_misc", - "should_report_agent_registry_spawn_gate_closed", - |ctx| { - Box::pin(async move { - let client = ctx.start_client().await; - - let err = client - .rpc() - .agent_registry() - .spawn(AgentRegistrySpawnRequest { - agent_name: None, - cwd: ctx.work_dir().to_string_lossy().to_string(), - initial_prompt: None, - model: None, - name: None, - permission_mode: None, - }) - .await - .expect_err("agent registry spawn should be gated"); - - let message = err.to_string(); - assert_not_unhandled(&message); - let lower = message.to_ascii_lowercase(); - assert!(lower.contains("agentregistry.spawn"), "{message}"); - assert!( - lower.contains("not enabled") || lower.contains("no delegate"), - "{message}" - ); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_shut_down_owned_runtime() { - with_e2e_context("rpc_server_misc", "should_shut_down_owned_runtime", |ctx| { - Box::pin(async move { - let client = Client::start(ctx.client_options()) - .await - .expect("start dedicated client"); - - client - .rpc() - .user() - .settings() - .reload() - .await - .expect("runtime should start live"); - - client - .rpc() - .runtime() - .shutdown() - .await - .expect("shut down runtime"); - - wait_for_condition("runtime to stop serving RPCs", || async { - client.rpc().user().settings().reload().await.is_err() - }) - .await; - - let _ = client.stop().await; - }) - }) - .await; -} - -#[tokio::test] -async fn should_report_not_found_when_opening_session_without_context() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server_misc", - "should_report_not_found_when_opening_session_without_context", - |ctx| { - Box::pin(async move { - let client = ctx.start_client().await; - - let result = client - .rpc() - .sessions() - .open() - .await - .expect("open session without context"); - - assert_eq!(result.status, SessionsOpenStatus::NotFound); - assert!(result.session_id.is_none()); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_reject_send_attachments_from_non_extension_connection() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server_misc", - "should_reject_send_attachments_from_non_extension_connection", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let err = session - .rpc() - .extensions() - .send_attachments_to_message(SendAttachmentsToMessageParams { - attachments: Vec::new(), - instance_id: None, - }) - .await - .expect_err("normal session connection should be rejected"); - let message = err.to_string(); - assert_not_unhandled(&message); - assert!( - message.to_ascii_lowercase().contains("extension"), - "{message}" - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -fn assert_not_unhandled(message: &str) { - assert!( - !message.to_ascii_lowercase().contains("unhandled method"), - "{message}" - ); -} - -fn setting_patch(key: &str, value: Value) -> Value { - let mut settings = Map::new(); - settings.insert(key.to_string(), value); - Value::Object(settings) -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_server_misc", 5); diff --git a/rust/tests/e2e/rpc_server_plugins.rs b/rust/tests/e2e/rpc_server_plugins.rs deleted file mode 100644 index f180bd1562..0000000000 --- a/rust/tests/e2e/rpc_server_plugins.rs +++ /dev/null @@ -1,551 +0,0 @@ -use std::fs; -use std::path::Path; - -use github_copilot_sdk::rpc::{ - InstalledPluginInfo, PluginListResult, PluginsDisableRequest, PluginsEnableRequest, - PluginsInstallRequest, PluginsMarketplacesAddRequest, PluginsMarketplacesBrowseRequest, - PluginsMarketplacesRefreshRequest, PluginsMarketplacesRemoveRequest, PluginsUninstallRequest, - PluginsUpdateRequest, -}; - -const MARKETPLACE_NAME: &str = "csharp-e2e-marketplace"; -const PLUGIN_NAME: &str = "csharp-e2e-plugin"; -const DIRECT_PLUGIN_NAME: &str = "csharp-e2e-direct"; - -#[tokio::test] -async fn should_install_and_list_plugin_from_local_marketplace() { - super::support::with_dedicated_group_e2e_context( - &E2E, - "rpc_server_plugins", - "should_install_and_list_plugin_from_local_marketplace", - |ctx| { - Box::pin(async move { - let marketplace = create_local_marketplace_fixture(); - let client = ctx.start_client().await; - let spec = format!("{PLUGIN_NAME}@{MARKETPLACE_NAME}"); - - client - .rpc() - .plugins() - .marketplaces() - .add(PluginsMarketplacesAddRequest { - source: marketplace.source(), - working_directory: None, - }) - .await - .expect("add marketplace"); - - let install = client - .rpc() - .plugins() - .install(PluginsInstallRequest { - source: spec, - working_directory: None, - }) - .await - .expect("install marketplace plugin"); - - assert_eq!(install.plugin.name, PLUGIN_NAME); - assert_eq!(install.plugin.marketplace, MARKETPLACE_NAME); - assert!(install.plugin.enabled); - assert!(install.skills_installed >= 1); - assert!(install.deprecation_warning.is_none()); - - let after_install = client.rpc().plugins().list().await.expect("list plugins"); - let listed = single_plugin(&after_install, PLUGIN_NAME, MARKETPLACE_NAME); - assert!(listed.enabled); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_enable_and_disable_marketplace_plugin() { - super::support::with_dedicated_group_e2e_context( - &E2E, - "rpc_server_plugins", - "should_enable_and_disable_marketplace_plugin", - |ctx| { - Box::pin(async move { - let marketplace = create_local_marketplace_fixture(); - let client = ctx.start_client().await; - let spec = format!("{PLUGIN_NAME}@{MARKETPLACE_NAME}"); - - client - .rpc() - .plugins() - .marketplaces() - .add(PluginsMarketplacesAddRequest { - source: marketplace.source(), - working_directory: None, - }) - .await - .expect("add marketplace"); - client - .rpc() - .plugins() - .install(PluginsInstallRequest { - source: spec.clone(), - working_directory: None, - }) - .await - .expect("install marketplace plugin"); - - client - .rpc() - .plugins() - .disable(PluginsDisableRequest { - names: vec![spec.clone()], - working_directory: None, - }) - .await - .expect("disable plugin"); - assert!( - !single_plugin( - &client.rpc().plugins().list().await.expect("list disabled"), - PLUGIN_NAME, - MARKETPLACE_NAME - ) - .enabled - ); - - client - .rpc() - .plugins() - .enable(PluginsEnableRequest { - names: vec![spec], - working_directory: None, - }) - .await - .expect("enable plugin"); - assert!( - single_plugin( - &client.rpc().plugins().list().await.expect("list enabled"), - PLUGIN_NAME, - MARKETPLACE_NAME - ) - .enabled - ); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_update_single_marketplace_plugin() { - super::support::with_dedicated_group_e2e_context( - &E2E, - "rpc_server_plugins", - "should_update_single_marketplace_plugin", - |ctx| { - Box::pin(async move { - let marketplace = create_local_marketplace_fixture(); - let client = ctx.start_client().await; - let spec = format!("{PLUGIN_NAME}@{MARKETPLACE_NAME}"); - - client - .rpc() - .plugins() - .marketplaces() - .add(PluginsMarketplacesAddRequest { - source: marketplace.source(), - working_directory: None, - }) - .await - .expect("add marketplace"); - client - .rpc() - .plugins() - .install(PluginsInstallRequest { - source: spec.clone(), - working_directory: None, - }) - .await - .expect("install marketplace plugin"); - - let update = client - .rpc() - .plugins() - .update(PluginsUpdateRequest { name: spec }) - .await - .expect("update plugin"); - - assert!(update.skills_installed >= 1); - assert_eq!(update.previous_version.as_deref(), Some("1.0.0")); - assert_eq!(update.new_version.as_deref(), Some("1.0.0")); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_update_all_installed_plugins() { - super::support::with_dedicated_group_e2e_context( - &E2E, - "rpc_server_plugins", - "should_update_all_installed_plugins", - |ctx| { - Box::pin(async move { - let marketplace = create_local_marketplace_fixture(); - let client = ctx.start_client().await; - let spec = format!("{PLUGIN_NAME}@{MARKETPLACE_NAME}"); - - client - .rpc() - .plugins() - .marketplaces() - .add(PluginsMarketplacesAddRequest { - source: marketplace.source(), - working_directory: None, - }) - .await - .expect("add marketplace"); - client - .rpc() - .plugins() - .install(PluginsInstallRequest { - source: spec, - working_directory: None, - }) - .await - .expect("install marketplace plugin"); - - let result = client - .rpc() - .plugins() - .update_all() - .await - .expect("update all plugins"); - - let matches: Vec<_> = result - .results - .iter() - .filter(|entry| { - entry.name == PLUGIN_NAME && entry.marketplace == MARKETPLACE_NAME - }) - .collect(); - assert_eq!(matches.len(), 1, "expected one update entry: {result:?}"); - let entry = matches[0]; - assert!(entry.success, "{:?}", entry.error); - assert!(entry.skills_installed.unwrap_or_default() >= 1); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_install_direct_local_plugin_with_deprecation_warning() { - super::support::with_dedicated_group_e2e_context( - &E2E, - "rpc_server_plugins", - "should_install_direct_local_plugin_with_deprecation_warning", - |ctx| { - Box::pin(async move { - let plugin = create_direct_plugin_fixture(); - let client = ctx.start_client().await; - - let install = client - .rpc() - .plugins() - .install(PluginsInstallRequest { - source: plugin.source(), - working_directory: None, - }) - .await - .expect("install direct plugin"); - - assert_eq!(install.plugin.name, DIRECT_PLUGIN_NAME); - assert_eq!(install.plugin.marketplace, ""); - let warning = install - .deprecation_warning - .as_deref() - .expect("direct installs should warn"); - assert!(warning.to_ascii_lowercase().contains("deprecated")); - assert!(install.skills_installed >= 1); - - let after_install = client.rpc().plugins().list().await.expect("list plugins"); - let direct_matches = after_install - .plugins - .iter() - .filter(|plugin| plugin.name == DIRECT_PLUGIN_NAME) - .count(); - assert_eq!( - direct_matches, 1, - "expected direct plugin in {after_install:?}" - ); - let direct_source_id = install.plugin.direct_source_id.clone(); - assert!( - direct_source_id.is_some(), - "expected direct plugin install to include direct_source_id" - ); - - client - .rpc() - .plugins() - .uninstall(PluginsUninstallRequest { - direct_source_id, - name: DIRECT_PLUGIN_NAME.to_string(), - }) - .await - .expect("uninstall direct plugin"); - - let after_uninstall = client - .rpc() - .plugins() - .list() - .await - .expect("list after uninstall"); - assert!( - !after_uninstall - .plugins - .iter() - .any(|plugin| plugin.name == DIRECT_PLUGIN_NAME) - ); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_list_browse_refresh_and_remove_local_marketplace() { - super::support::with_dedicated_group_e2e_context( - &E2E, - "rpc_server_plugins", - "should_list_browse_refresh_and_remove_local_marketplace", - |ctx| { - Box::pin(async move { - let marketplace = create_local_marketplace_fixture(); - let client = ctx.start_client().await; - - let add = client - .rpc() - .plugins() - .marketplaces() - .add(PluginsMarketplacesAddRequest { - source: marketplace.source(), - working_directory: None, - }) - .await - .expect("add marketplace"); - assert_eq!(add.name, MARKETPLACE_NAME); - - let list = client - .rpc() - .plugins() - .marketplaces() - .list() - .await - .expect("list marketplaces"); - let mine: Vec<_> = list - .marketplaces - .iter() - .filter(|marketplace| marketplace.name == MARKETPLACE_NAME) - .collect(); - assert_eq!(mine.len(), 1, "expected local marketplace in {list:?}"); - assert_ne!(mine[0].is_default, Some(true)); - assert!( - list.marketplaces - .iter() - .any(|marketplace| marketplace.is_default == Some(true)) - ); - - let browse = client - .rpc() - .plugins() - .marketplaces() - .browse(PluginsMarketplacesBrowseRequest { - name: MARKETPLACE_NAME.to_string(), - }) - .await - .expect("browse marketplace"); - let advertised: Vec<_> = browse - .plugins - .iter() - .filter(|plugin| plugin.name == PLUGIN_NAME) - .collect(); - assert_eq!( - advertised.len(), - 1, - "expected advertised plugin in {browse:?}" - ); - assert!( - advertised[0] - .description - .as_deref() - .is_some_and(|description| !description.is_empty()) - ); - - let refresh = client - .rpc() - .plugins() - .marketplaces() - .refresh_with_params(PluginsMarketplacesRefreshRequest { - name: Some(MARKETPLACE_NAME.to_string()), - }) - .await - .expect("refresh marketplace"); - let refreshed: Vec<_> = refresh - .results - .iter() - .filter(|entry| entry.name == MARKETPLACE_NAME) - .collect(); - assert_eq!(refreshed.len(), 1, "expected refresh result in {refresh:?}"); - assert!(refreshed[0].success, "{:?}", refreshed[0].error); - - let remove = client - .rpc() - .plugins() - .marketplaces() - .remove(PluginsMarketplacesRemoveRequest { - force: None, - name: MARKETPLACE_NAME.to_string(), - }) - .await - .expect("remove marketplace"); - assert!(remove.removed); - - let after_remove = client - .rpc() - .plugins() - .marketplaces() - .list() - .await - .expect("list after remove"); - assert!( - !after_remove - .marketplaces - .iter() - .any(|marketplace| marketplace.name == MARKETPLACE_NAME) - ); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_reload_mcp_config_cache() { - super::support::with_dedicated_group_e2e_context( - &E2E, - "rpc_server_plugins", - "should_reload_mcp_config_cache", - |ctx| { - Box::pin(async move { - let client = ctx.start_client().await; - client - .rpc() - .mcp() - .config() - .reload() - .await - .expect("reload MCP config cache"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -struct LocalFixture { - dir: tempfile::TempDir, -} - -impl LocalFixture { - fn source(&self) -> String { - self.dir.path().to_string_lossy().to_string() - } -} - -fn create_local_marketplace_fixture() -> LocalFixture { - let dir = tempfile::Builder::new() - .prefix("copilot-e2e-mp-") - .tempdir() - .expect("create local marketplace fixture"); - let manifest = format!( - r#"{{ - "name": "{MARKETPLACE_NAME}", - "owner": {{ "name": "Copilot SDK E2E" }}, - "metadata": {{ "description": "Local marketplace fixture for SDK E2E tests." }}, - "plugins": [ - {{ - "name": "{PLUGIN_NAME}", - "source": "./{PLUGIN_NAME}", - "description": "E2E demo plugin advertised by the local marketplace.", - "version": "1.0.0" - }} - ] -}} -"# - ); - fs::write(dir.path().join("marketplace.json"), manifest).expect("write marketplace manifest"); - - let plugin_dir = dir.path().join(PLUGIN_NAME); - fs::create_dir_all(&plugin_dir).expect("create marketplace plugin directory"); - write_skill_file(&plugin_dir); - - LocalFixture { dir } -} - -fn create_direct_plugin_fixture() -> LocalFixture { - let dir = tempfile::Builder::new() - .prefix("copilot-e2e-plugin-") - .tempdir() - .expect("create direct plugin fixture"); - let manifest = format!( - r#"{{ - "name": "{DIRECT_PLUGIN_NAME}", - "description": "E2E demo plugin installed directly from a local path.", - "version": "1.0.0" -}} -"# - ); - fs::write(dir.path().join("plugin.json"), manifest).expect("write plugin manifest"); - write_skill_file(dir.path()); - - LocalFixture { dir } -} - -fn write_skill_file(plugin_dir: &Path) { - let skill = r#"--- -name: csharp-e2e-skill -description: A demo skill contributed by the E2E test plugin. ---- -# Demo Skill - -This skill exists so the plugin reports at least one installed skill. -"#; - fs::write(plugin_dir.join("SKILL.md"), skill).expect("write skill file"); -} - -fn single_plugin<'a>( - list: &'a PluginListResult, - name: &str, - marketplace: &str, -) -> &'a InstalledPluginInfo { - let matches: Vec<_> = list - .plugins - .iter() - .filter(|plugin| plugin.name == name && plugin.marketplace == marketplace) - .collect(); - assert_eq!(matches.len(), 1, "expected one plugin in {list:?}"); - matches[0] -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_server_plugins", 7); diff --git a/rust/tests/e2e/rpc_server_remote_control.rs b/rust/tests/e2e/rpc_server_remote_control.rs deleted file mode 100644 index 49809235cc..0000000000 --- a/rust/tests/e2e/rpc_server_remote_control.rs +++ /dev/null @@ -1,184 +0,0 @@ -use github_copilot_sdk::SessionId; -use github_copilot_sdk::rpc::{ - RemoteControlConfig, SessionsSetRemoteControlSteeringRequest, - SessionsStartRemoteControlRequest, SessionsStopRemoteControlRequest, - SessionsTransferRemoteControlRequest, -}; -use serde_json::Value; - -#[tokio::test] -async fn should_report_remote_control_status_as_off() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server_remote_control", - "should_report_remote_control_status_as_off", - |ctx| { - Box::pin(async move { - let client = ctx.start_client().await; - - let result = client - .rpc() - .sessions() - .get_remote_control_status() - .await - .expect("get remote control status"); - - assert_status_off(&result.status); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_treat_set_steering_as_no_op_when_off() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server_remote_control", - "should_treat_set_steering_as_no_op_when_off", - |ctx| { - Box::pin(async move { - let client = ctx.start_client().await; - - let result = client - .rpc() - .sessions() - .set_remote_control_steering(SessionsSetRemoteControlSteeringRequest { - enabled: false, - }) - .await - .expect("set remote control steering"); - - assert_status_off(&result.status); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_not_stopped_when_remote_control_is_off() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server_remote_control", - "should_report_not_stopped_when_remote_control_is_off", - |ctx| { - Box::pin(async move { - let client = ctx.start_client().await; - - let result = client - .rpc() - .sessions() - .stop_remote_control() - .await - .expect("stop remote control"); - - assert!(!result.stopped); - assert_status_off(&result.status); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_reject_transfer_when_off_with_compare_and_swap() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server_remote_control", - "should_reject_transfer_when_off_with_compare_and_swap", - |ctx| { - Box::pin(async move { - let client = ctx.start_client().await; - - let result = client - .rpc() - .sessions() - .transfer_remote_control(SessionsTransferRemoteControlRequest { - expected_from_session_id: Some(format!( - "rc-from-{}", - uuid::Uuid::new_v4().simple() - )), - to_session_id: format!("rc-to-{}", uuid::Uuid::new_v4().simple()), - }) - .await - .expect("transfer remote control"); - - assert!(!result.transferred); - assert_status_off(&result.status); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_reach_runtime_when_starting_remote_control_for_unknown_session() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_server_remote_control", - "should_reach_runtime_when_starting_remote_control_for_unknown_session", - |ctx| { - Box::pin(async move { - let client = ctx.start_client().await; - - let result = client - .rpc() - .sessions() - .start_remote_control(SessionsStartRemoteControlRequest { - session_id: SessionId::from(format!( - "missing-session-{}", - uuid::Uuid::new_v4().simple() - )), - config: RemoteControlConfig { - existing_mc_session: None, - explicit: false, - remote: false, - silent: true, - steerable: false, - task_id: None, - }, - }) - .await; - - let _ = client - .rpc() - .sessions() - .stop_remote_control_with_params(SessionsStopRemoteControlRequest { - expected_session_id: None, - force: Some(true), - }) - .await; - - let err = result.expect_err("unknown session should fail"); - let message = err.to_string(); - assert_not_unhandled(&message); - let lower = message.to_ascii_lowercase(); - assert!( - lower.contains("session") || lower.contains("remote"), - "{message}" - ); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -fn assert_status_off(status: &Value) { - assert_eq!(status.get("state").and_then(Value::as_str), Some("off")); -} - -fn assert_not_unhandled(message: &str) { - assert!( - !message.to_ascii_lowercase().contains("unhandled method"), - "{message}" - ); -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_server_remote_control", 5); diff --git a/rust/tests/e2e/rpc_session_state.rs b/rust/tests/e2e/rpc_session_state.rs deleted file mode 100644 index e4bb285edb..0000000000 --- a/rust/tests/e2e/rpc_session_state.rs +++ /dev/null @@ -1,1268 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; - -use async_trait::async_trait; -use github_copilot_sdk::rpc::{ - AuthInfoType, HistoryTruncateRequest, LspInitializeRequest, MetadataContextInfoRequest, - MetadataRecomputeContextTokensRequest, MetadataRecordContextChangeRequest, - MetadataSetWorkingDirectoryRequest, MetadataSnapshotCurrentMode, ModeSetRequest, - ModelSetReasoningEffortRequest, ModelSwitchToRequest, NameSetAutoRequest, NameSetRequest, - PermissionsResetSessionApprovalsRequest, PermissionsSetApproveAllRequest, PlanUpdateRequest, - SessionSetCredentialsParams, SessionUpdateOptionsParams, SessionWorkingDirectoryContext, - SessionWorkingDirectoryContextHostType, SessionsForkRequest, SettableAuthInfo, ShutdownRequest, - TelemetrySetFeatureOverridesRequest, UserAuthInfo, WorkspacesCreateFileRequest, - WorkspacesReadFileRequest, -}; -use github_copilot_sdk::session_events::{ - SessionContextChangedData, SessionEventType, SessionMode, SessionShutdownData, - SessionTitleChangedData, SessionWorkspaceFileChangedData, ShutdownType, - WorkspaceFileChangedOperation, -}; -use github_copilot_sdk::tool::ToolHandler; -use github_copilot_sdk::{Error, Tool, ToolInvocation, ToolResult}; -use serde_json::json; -use tokio::sync::{Mutex, mpsc, oneshot}; - -use super::support::{ - assistant_message_content, recv_with_timeout, wait_for_condition, wait_for_event, -}; - -const MODEL_ID: &str = "claude-sonnet-5"; - -#[tokio::test] -async fn should_call_session_rpc_model_getcurrent() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_call_session_rpc_model_getcurrent", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config().with_model(MODEL_ID)) - .await - .expect("create session"); - - let current = session - .rpc() - .model() - .get_current() - .await - .expect("get current model"); - assert_eq!(current.model_id.as_deref(), Some(MODEL_ID)); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_call_session_rpc_model_switchto() { - super::support::with_dedicated_group_e2e_context( - &E2E, - "rpc_session_state", - "should_call_session_rpc_model_switchto", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config().with_model(MODEL_ID)) - .await - .expect("create session"); - - let before = session - .rpc() - .model() - .get_current() - .await - .expect("get current model before switch"); - assert!(before.model_id.is_some(), "expected a model before switch"); - - let switched = session - .rpc() - .model() - .switch_to(ModelSwitchToRequest { - model_id: "gpt-5.4".to_string(), - reasoning_effort: Some("high".to_string()), - model_capabilities: None, - reasoning_summary: None, - ..Default::default() - }) - .await - .expect("switch model"); - assert_eq!(switched.model_id.as_deref(), Some("gpt-5.4")); - - let after = session - .rpc() - .model() - .get_current() - .await - .expect("get current model after switch"); - assert_eq!(after.model_id.as_deref(), Some("gpt-5.4")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_get_and_set_session_mode() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_get_and_set_session_mode", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - assert_eq!( - session.rpc().mode().get().await.expect("get initial mode"), - SessionMode::Interactive - ); - session - .rpc() - .mode() - .set(ModeSetRequest { - mode: SessionMode::Plan, - ..Default::default() - }) - .await - .expect("set plan mode"); - assert_eq!( - session.rpc().mode().get().await.expect("get plan mode"), - SessionMode::Plan - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_shutdown_session_with_routine_type() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_shutdown_session_with_routine_type", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let shutdown = wait_for_event(session.subscribe(), "session shutdown", |event| { - event.parsed_type() == SessionEventType::SessionShutdown - }); - - session - .rpc() - .shutdown(ShutdownRequest { - reason: Some("routine rust rpc test".to_string()), - r#type: Some(ShutdownType::Routine), - ..Default::default() - }) - .await - .expect("shutdown session"); - let data = shutdown - .await - .typed_data::() - .expect("shutdown data"); - assert_eq!(data.shutdown_type, ShutdownType::Routine); - - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_set_and_get_each_session_mode_value() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_set_and_get_each_session_mode_value", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - for mode in [ - SessionMode::Interactive, - SessionMode::Plan, - SessionMode::Autopilot, - ] { - session - .rpc() - .mode() - .set(ModeSetRequest { - mode: mode.clone(), - ..Default::default() - }) - .await - .expect("set mode"); - assert_eq!(session.rpc().mode().get().await.expect("get mode"), mode); - } - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_read_update_and_delete_plan() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_read_update_and_delete_plan", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let initial = session - .rpc() - .plan() - .read() - .await - .expect("read initial plan"); - assert!(!initial.exists); - assert!(initial.content.is_none()); - - let content = "# Rust RPC plan\n- verify plan state"; - session - .rpc() - .plan() - .update(PlanUpdateRequest { - content: content.to_string(), - }) - .await - .expect("update plan"); - let updated = session - .rpc() - .plan() - .read() - .await - .expect("read updated plan"); - assert!(updated.exists); - assert_eq!(updated.content.as_deref(), Some(content)); - assert!( - updated - .path - .as_deref() - .is_some_and(|path| path.ends_with("plan.md")) - ); - - session.rpc().plan().delete().await.expect("delete plan"); - let deleted = session - .rpc() - .plan() - .read() - .await - .expect("read deleted plan"); - assert!(!deleted.exists); - assert!(deleted.content.is_none()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_call_workspace_file_rpc_methods() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_call_workspace_file_rpc_methods", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let before = session - .rpc() - .workspaces() - .list_files() - .await - .expect("list files before"); - assert!(before.files.is_empty()); - - session - .rpc() - .workspaces() - .create_file(WorkspacesCreateFileRequest { - path: "rpc-state-rust.txt".to_string(), - content: "workspace rpc content".to_string(), - }) - .await - .expect("create workspace file"); - let read = session - .rpc() - .workspaces() - .read_file(WorkspacesReadFileRequest { - path: "rpc-state-rust.txt".to_string(), - }) - .await - .expect("read workspace file"); - assert_eq!(read.content, "workspace rpc content"); - let workspace = session - .rpc() - .workspaces() - .get_workspace() - .await - .expect("get workspace"); - let workspace = workspace.workspace.expect("workspace details"); - assert!(!workspace.id.trim().is_empty()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_reject_workspace_file_path_traversal() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_reject_workspace_file_path_traversal", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - expect_err_contains( - session - .rpc() - .workspaces() - .create_file(WorkspacesCreateFileRequest { - path: "../escape.txt".to_string(), - content: "nope".to_string(), - }) - .await, - "workspace files directory", - ); - expect_err_contains( - session - .rpc() - .workspaces() - .read_file(WorkspacesReadFileRequest { - path: "../../escape.txt".to_string(), - }) - .await, - "workspace files directory", - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_create_workspace_file_with_nested_path_auto_creating_dirs() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_create_workspace_file_with_nested_path_auto_creating_dirs", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let path = "nested/rust/path/file.txt"; - - session - .rpc() - .workspaces() - .create_file(WorkspacesCreateFileRequest { - path: path.to_string(), - content: "nested content".to_string(), - }) - .await - .expect("create nested workspace file"); - let read = session - .rpc() - .workspaces() - .read_file(WorkspacesReadFileRequest { - path: path.to_string(), - }) - .await - .expect("read nested workspace file"); - assert_eq!(read.content, "nested content"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_error_reading_nonexistent_workspace_file() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_report_error_reading_nonexistent_workspace_file", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - assert!( - session - .rpc() - .workspaces() - .read_file(WorkspacesReadFileRequest { - path: "missing-rust-file.txt".to_string(), - }) - .await - .is_err() - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_update_existing_workspace_file_with_update_operation() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_update_existing_workspace_file_with_update_operation", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let path = "updated-rust.txt"; - - session - .rpc() - .workspaces() - .create_file(WorkspacesCreateFileRequest { - path: path.to_string(), - content: "first".to_string(), - }) - .await - .expect("create workspace file"); - let updated = - wait_for_event(session.subscribe(), "workspace file updated", |event| { - if event.parsed_type() != SessionEventType::SessionWorkspaceFileChanged { - return false; - } - event - .typed_data::() - .is_some_and(|data| { - data.path == path - && data.operation == WorkspaceFileChangedOperation::Update - }) - }); - session - .rpc() - .workspaces() - .create_file(WorkspacesCreateFileRequest { - path: path.to_string(), - content: "second".to_string(), - }) - .await - .expect("update workspace file"); - updated.await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_reject_empty_or_whitespace_session_name() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_reject_empty_or_whitespace_session_name", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - for name in ["", " \t"] { - expect_err_contains( - session - .rpc() - .name() - .set(NameSetRequest { - name: name.to_string(), - }) - .await, - "empty", - ); - } - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_emit_title_changed_event_each_time_name_set_is_called() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_emit_title_changed_event_each_time_name_set_is_called", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - for title in ["Rust RPC title", "Rust RPC title"] { - let changed = wait_for_event(session.subscribe(), "title changed", |event| { - event.parsed_type() == SessionEventType::SessionTitleChanged - && event - .typed_data::() - .is_some_and(|data| data.title == title) - }); - session - .rpc() - .name() - .set(NameSetRequest { - name: title.to_string(), - }) - .await - .expect("set title"); - changed.await; - } - assert_eq!( - session - .rpc() - .name() - .get() - .await - .expect("get title") - .name - .as_deref(), - Some("Rust RPC title") - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_get_and_set_session_metadata() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_call_metadata_snapshot_setworkingdirectory_and_recordcontextchange", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - session - .rpc() - .name() - .set(NameSetRequest { - name: "Rust metadata name".to_string(), - }) - .await - .expect("set name"); - assert_eq!( - session - .rpc() - .name() - .get() - .await - .expect("get name") - .name - .as_deref(), - Some("Rust metadata name") - ); - let sources = session - .rpc() - .instructions() - .get_sources() - .await - .expect("instruction sources"); - assert!(sources.sources.iter().all(|source| !source.id.is_empty())); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_call_metadata_snapshot_setworkingdirectory_and_recordcontextchange() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_get_and_set_session_metadata", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let subdir = ctx.work_dir().join("metadata-cwd"); - std::fs::create_dir_all(&subdir).expect("create metadata cwd"); - - let snapshot = session - .rpc() - .metadata() - .snapshot() - .await - .expect("metadata snapshot"); - assert_eq!(snapshot.session_id, session.id().clone()); - assert_eq!( - snapshot.current_mode, - MetadataSnapshotCurrentMode::Interactive - ); - assert!(!snapshot.start_time.is_empty()); - assert!(!snapshot.modified_time.is_empty()); - - let set = session - .rpc() - .metadata() - .set_working_directory(MetadataSetWorkingDirectoryRequest { - working_directory: subdir.display().to_string(), - }) - .await - .expect("set working directory"); - assert_paths_equal(&set.working_directory, &subdir); - - let changed = wait_for_event(session.subscribe(), "context changed", |event| { - event.parsed_type() == SessionEventType::SessionContextChanged - && event - .typed_data::() - .is_some_and(|data| { - data.repository.as_deref() == Some("github/copilot-sdk") - }) - }); - session - .rpc() - .metadata() - .record_context_change(MetadataRecordContextChangeRequest { - context: SessionWorkingDirectoryContext { - base_commit: None, - branch: Some("rust-rpc-e2e".to_string()), - cwd: subdir.display().to_string(), - git_root: Some(ctx.repo_root().display().to_string()), - head_commit: None, - host_type: Some(SessionWorkingDirectoryContextHostType::GitHub), - repository: Some("github/copilot-sdk".to_string()), - repository_host: Some("github.com".to_string()), - }, - }) - .await - .expect("record context change"); - let data = changed - .await - .typed_data::() - .expect("context changed data"); - assert_paths_equal(&data.cwd, &subdir); - assert_eq!(data.branch.as_deref(), Some("rust-rpc-e2e")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -#[allow( - clippy::field_reassign_with_default, - reason = "the generated request has a private field, and some feature profiles do not trigger this lint" -)] -async fn should_update_options_and_initialize_session_services() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_update_options_and_initialize_session_services", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let mut update_options = SessionUpdateOptionsParams::default(); - update_options.ask_user_disabled = Some(true); - update_options.available_tools = Some(vec!["view".to_string()]); - update_options.client_name = Some("rust-rpc-e2e".to_string()); - update_options.enable_streaming = Some(true); - update_options.model = Some(MODEL_ID.to_string()); - update_options.working_directory = Some(ctx.work_dir().display().to_string()); - - let options = session - .rpc() - .options() - .update(update_options) - .await - .expect("update options"); - assert!(options.success); - session - .rpc() - .lsp() - .initialize(LspInitializeRequest { - force: Some(true), - git_root: Some(ctx.repo_root().display().to_string()), - working_directory: Some(ctx.work_dir().display().to_string()), - }) - .await - .expect("initialize lsp"); - session - .rpc() - .telemetry() - .set_feature_overrides(TelemetrySetFeatureOverridesRequest { - features: HashMap::from([( - "rust-rpc-e2e".to_string(), - "enabled".to_string(), - )]), - }) - .await - .expect("set telemetry overrides"); - session - .rpc() - .tools() - .initialize_and_validate() - .await - .expect("initialize tools"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_set_reasoningeffort_and_auto_name() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_set_reasoningeffort_and_auto_name", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let effort = session - .rpc() - .model() - .set_reasoning_effort(ModelSetReasoningEffortRequest { - reasoning_effort: "none".to_string(), - }) - .await - .expect("set reasoning effort"); - assert_eq!(effort.reasoning_effort, "none"); - let auto = session - .rpc() - .name() - .set_auto(NameSetAutoRequest { - summary: "Rust auto title".to_string(), - }) - .await - .expect("set auto name"); - assert!(auto.applied); - session - .rpc() - .name() - .set(NameSetRequest { - name: "Explicit Rust title".to_string(), - }) - .await - .expect("set explicit name"); - let not_applied = session - .rpc() - .name() - .set_auto(NameSetAutoRequest { - summary: "Ignored auto title".to_string(), - }) - .await - .expect("set ignored auto name"); - assert!(!not_applied.applied); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_set_auth_credentials() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_set_auth_credentials", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let token = "rpc-session-auth-token"; - ctx.set_copilot_user_by_token_with_login(token, "rpc-session-user"); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let set = session - .rpc() - .git_hub_auth() - .set_credentials(SessionSetCredentialsParams { - credentials: Some(SettableAuthInfo::User(UserAuthInfo { - host: "github.com".to_string(), - login: "rpc-session-user".to_string(), - ..Default::default() - })), - }) - .await - .expect("set credentials"); - assert!(set.success); - let status = session - .rpc() - .git_hub_auth() - .get_status() - .await - .expect("auth status"); - assert!(status.is_authenticated); - assert_eq!(status.auth_type, Some(AuthInfoType::User)); - assert_eq!(status.host.as_deref(), Some("github.com")); - assert_eq!(status.login.as_deref(), Some("rpc-session-user")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_fork_session_with_persisted_messages() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_fork_session_with_persisted_messages", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let answer = session - .send_and_wait("Say FORK_SOURCE_ALPHA exactly.") - .await - .expect("send") - .expect("assistant response"); - assert!(assistant_message_content(&answer).contains("FORK_SOURCE_ALPHA")); - - let fork = client - .rpc() - .sessions() - .fork(SessionsForkRequest { - session_id: session.id().clone(), - to_event_id: None, - name: Some("Rust fork".to_string()), - }) - .await - .expect("fork session"); - assert_ne!(fork.session_id, session.id().clone()); - assert_eq!(fork.name.as_deref(), Some("Rust fork")); - let forked = client - .resume_session( - github_copilot_sdk::ResumeSessionConfig::new(fork.session_id.clone()) - .with_github_token(super::support::DEFAULT_TEST_TOKEN), - ) - .await - .expect("resume fork"); - assert!( - forked - .get_events() - .await - .expect("fork events") - .iter() - .any(|event| assistant_message_content_if_present(event) - .is_some_and(|content| content.contains("FORK_SOURCE_ALPHA"))) - ); - - forked.disconnect().await.expect("disconnect fork"); - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_error_when_forking_session_to_unknown_event_id() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_report_error_when_forking_session_to_unknown_event_id", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let err = client - .rpc() - .sessions() - .fork(SessionsForkRequest { - session_id: session.id().clone(), - to_event_id: Some("missing-event-id".to_string()), - name: None, - }) - .await - .expect_err("unknown boundary should fail"); - let message = err.to_string(); - assert!(message.contains("missing-event-id") || message.contains("not found")); - assert!(!message.contains("Unhandled method")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_call_session_usage_and_permission_rpcs() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_call_session_usage_and_permission_rpcs", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let metrics = session.rpc().usage().get_metrics().await.expect("usage"); - assert!(!metrics.session_start_time.is_empty()); - assert_eq!(metrics.total_user_requests, 0); - assert!( - session - .rpc() - .permissions() - .set_approve_all(PermissionsSetApproveAllRequest { - enabled: true, - source: None, - }) - .await - .expect("enable approve all") - .success - ); - assert!( - session - .rpc() - .permissions() - .reset_session_approvals(PermissionsResetSessionApprovalsRequest::default()) - .await - .expect("reset approvals") - .success - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_implemented_errors_for_unsupported_session_rpc_paths() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_report_implemented_errors_for_unsupported_session_rpc_paths", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let truncate = session - .rpc() - .history() - .truncate(HistoryTruncateRequest { - event_id: "missing-event-id".to_string(), - }) - .await - .expect_err("truncate missing event should fail"); - assert!( - !truncate - .to_string() - .contains("Unhandled method session.history.truncate") - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_processing_and_context_metadata() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state", - "should_report_processing_and_context_metadata", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let (entered_tx, mut entered_rx) = mpsc::unbounded_channel(); - let (release_tx, release_rx) = oneshot::channel(); - let session = client - .create_session( - ctx.approve_all_session_config() - .with_model(MODEL_ID) - .with_tools(vec![ - Tool::new("processing_barrier") - .with_description( - "Blocks until the processing state has been observed", - ) - .with_parameters(json!({ - "type": "object", - "properties": {}, - "additionalProperties": false - })) - .with_handler(Arc::new(ProcessingBarrierTool { - entered_tx, - release_rx: Mutex::new(Some(release_rx)), - })), - ]), - ) - .await - .expect("create session"); - - assert!( - !session - .rpc() - .metadata() - .is_processing() - .await - .expect("processing before send") - .processing - ); - session - .send("Use processing_barrier, then reply with exactly: RUST_CONTEXT_INFO") - .await - .expect("send"); - recv_with_timeout(&mut entered_rx, "processing barrier entry").await; - assert!( - session - .rpc() - .metadata() - .is_processing() - .await - .expect("processing during tool call") - .processing - ); - release_tx.send(()).expect("release processing barrier"); - wait_for_condition("session processing completed", || async { - !session - .rpc() - .metadata() - .is_processing() - .await - .expect("processing poll") - .processing - }) - .await; - let context = session - .rpc() - .metadata() - .context_info(MetadataContextInfoRequest { - prompt_token_limit: 200_000, - output_token_limit: 4096, - selected_model: Some(MODEL_ID.to_string()), - }) - .await - .expect("context info"); - let context_info = context.context_info.expect("context info"); - assert_eq!(context_info.model_name, MODEL_ID); - let recomputed = session - .rpc() - .metadata() - .recompute_context_tokens(MetadataRecomputeContextTokensRequest { - model_id: MODEL_ID.to_string(), - }) - .await - .expect("recompute context tokens"); - assert!(recomputed.total_tokens >= recomputed.messages_token_count); - assert!(recomputed.total_tokens >= recomputed.system_token_count); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -struct ProcessingBarrierTool { - entered_tx: mpsc::UnboundedSender<()>, - release_rx: Mutex>>, -} - -#[async_trait] -impl ToolHandler for ProcessingBarrierTool { - async fn call(&self, _invocation: ToolInvocation) -> Result { - let _ = self.entered_tx.send(()); - self.release_rx - .lock() - .await - .take() - .expect("processing barrier called once") - .await - .expect("processing barrier released"); - Ok(ToolResult::Text("PROCESSING_OBSERVED".to_string())) - } -} - -fn expect_err_contains(result: Result, expected: &str) { - let err = match result { - Ok(_) => panic!("expected error containing {expected:?}"), - Err(err) => err, - }; - assert!( - err.to_string() - .to_ascii_lowercase() - .contains(&expected.to_ascii_lowercase()), - "expected error to contain {expected:?}, got {err}" - ); -} - -fn assert_paths_equal(actual: &str, expected: &std::path::Path) { - let actual = std::path::Path::new(actual); - assert_eq!( - std::fs::canonicalize(actual).unwrap_or_else(|_| actual.to_path_buf()), - std::fs::canonicalize(expected).unwrap_or_else(|_| expected.to_path_buf()) - ); -} - -fn assistant_message_content_if_present( - event: &github_copilot_sdk::SessionEvent, -) -> Option { - if event.parsed_type() == SessionEventType::AssistantMessage { - Some(assistant_message_content(event)) - } else { - None - } -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_session_state", 22); diff --git a/rust/tests/e2e/rpc_session_state_extras.rs b/rust/tests/e2e/rpc_session_state_extras.rs deleted file mode 100644 index 127a73ddaa..0000000000 --- a/rust/tests/e2e/rpc_session_state_extras.rs +++ /dev/null @@ -1,569 +0,0 @@ -use std::collections::HashMap; - -use github_copilot_sdk::Client; -use github_copilot_sdk::rpc::{ - CompletionsRequestRequest, MetadataContextHeaviestMessagesRequest, ModelSwitchToRequest, - NamedProviderConfig, PermissionsSetModeRequest, ProviderAddRequest, ProviderConfigType, - ProviderConfigWireApi, ProviderModelConfig, SessionVisibilityStatus, SubagentSettingsEntry, - SubagentSettingsEntryContextTier, UpdateSubagentSettingsRequest, - UpdateSubagentSettingsRequestSubagents, VisibilitySetRequest, -}; -use github_copilot_sdk::session_events::PermissionMode; - -use super::support::{assistant_message_content, with_e2e_context}; - -const MODEL_ID: &str = "claude-sonnet-5"; - -#[tokio::test] -async fn should_list_models_for_session() { - with_e2e_context( - "rpc_session_state_extras", - "should_list_models_for_session", - |ctx| { - Box::pin(async move { - let token = "rpc-session-model-list-token"; - ctx.set_copilot_user_by_token_with_login(token, "rpc-session-extras-user"); - let client = Client::start(ctx.client_options_with_github_token(token)) - .await - .expect("start authenticated client"); - let session = client - .create_session( - ctx.approve_all_session_config() - .with_github_token(token) - .with_model(MODEL_ID), - ) - .await - .expect("create session"); - - let result = session.rpc().model().list().await.expect("list models"); - - assert!(!result.list.is_empty()); - assert!( - result - .list - .iter() - .any(|model| model.to_string().contains(MODEL_ID)) - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_session_activity_when_idle() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state_extras", - "should_report_session_activity_when_idle", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let activity = session - .rpc() - .metadata() - .activity() - .await - .expect("get activity"); - - assert!(!activity.has_active_work); - assert!(!activity.abortable); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_get_and_set_allowall_permissions() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state_extras", - "should_get_and_set_allowall_permissions", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let initial = session - .rpc() - .permissions() - .get_mode() - .await - .expect("get initial permission mode"); - assert_eq!(initial.mode, PermissionMode::Manual); - - let enable = session - .rpc() - .permissions() - .set_mode(PermissionsSetModeRequest { - assisted_approval_model: None, - mode: PermissionMode::AllowAll, - source: None, - }) - .await - .expect("set allow-all mode"); - assert!(enable.success); - assert_eq!(enable.mode, PermissionMode::AllowAll); - assert_eq!( - session - .rpc() - .permissions() - .get_mode() - .await - .expect("get allow-all mode") - .mode, - PermissionMode::AllowAll - ); - - let disable = session - .rpc() - .permissions() - .set_mode(PermissionsSetModeRequest { - assisted_approval_model: None, - mode: PermissionMode::Manual, - source: None, - }) - .await - .expect("set manual mode"); - assert!(disable.success); - assert_eq!(disable.mode, PermissionMode::Manual); - assert_eq!( - session - .rpc() - .permissions() - .get_mode() - .await - .expect("get manual mode") - .mode, - PermissionMode::Manual - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_read_empty_sql_todos_for_fresh_session() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state_extras", - "should_read_empty_sql_todos_for_fresh_session", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let result = session - .rpc() - .plan() - .read_sql_todos() - .await - .expect("read SQL todos"); - - assert!(result.rows.is_empty()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_get_telemetry_engagement_id() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state_extras", - "should_get_telemetry_engagement_id", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let _result = session - .rpc() - .telemetry() - .get_engagement_id() - .await - .expect("get telemetry engagement id"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_get_current_tool_metadata_after_initialization() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state_extras", - "should_get_current_tool_metadata_after_initialization", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let answer = session - .send_and_wait("What is 2+2?") - .await - .expect("send prompt") - .expect("assistant message"); - assert!(!assistant_message_content(&answer).trim().is_empty()); - - let result = session - .rpc() - .tools() - .get_current_metadata() - .await - .expect("get current tool metadata"); - - let tools = result.tools.expect("current tool metadata"); - assert!(!tools.is_empty()); - assert!(tools.iter().all(|tool| !tool.name.trim().is_empty())); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_add_byok_provider_and_model_at_runtime() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state_extras", - "should_add_byok_provider_and_model_at_runtime", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let result = session - .rpc() - .provider() - .add(ProviderAddRequest { - providers: Some(vec![NamedProviderConfig { - api_key: Some("provider-key".to_string()), - azure: None, - base_url: "https://models.example.test/v1".to_string(), - bearer_token: None, - has_bearer_token_provider: None, - headers: Some(HashMap::from([( - "x-provider".to_string(), - "rust".to_string(), - )])), - name: "rust-e2e-provider".to_string(), - transport: None, - r#type: Some(ProviderConfigType::Openai), - wire_api: Some(ProviderConfigWireApi::Completions), - }]), - models: Some(vec![ProviderModelConfig { - capabilities: None, - id: "small".to_string(), - max_context_window_tokens: None, - max_output_tokens: None, - max_prompt_tokens: Some(4096), - metadata: None, - model_id: None, - name: Some("Rust Added Model".to_string()), - provider: "rust-e2e-provider".to_string(), - system_message: None, - wire_model: None, - }]), - }) - .await - .expect("add provider model"); - assert_eq!(result.models.len(), 1); - - let selection_id = "rust-e2e-provider/small"; - session - .rpc() - .model() - .switch_to(ModelSwitchToRequest { - model_id: selection_id.to_string(), - ..Default::default() - }) - .await - .expect("switch to added model"); - let current = session - .rpc() - .model() - .get_current() - .await - .expect("get current model"); - assert_eq!(current.model_id.as_deref(), Some(selection_id)); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_return_empty_completions_when_host_does_not_provide_them() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state_extras", - "should_return_empty_completions_when_host_does_not_provide_them", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let result = session - .rpc() - .completions() - .request(CompletionsRequestRequest { - offset: 5, - text: "Use @ to mention context".to_string(), - }) - .await - .expect("request completions"); - assert!(result.items.is_empty()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_visibility_as_unsynced_for_local_session() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state_extras", - "should_report_visibility_as_unsynced_for_local_session", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let set = session - .rpc() - .visibility() - .set(VisibilitySetRequest { - status: SessionVisibilityStatus::Unshared, - }) - .await - .expect("set visibility"); - assert!(!set.synced); - assert!(set.status.is_none()); - assert!(set.share_url.is_none()); - let get = session - .rpc() - .visibility() - .get() - .await - .expect("get visibility"); - assert!(!get.synced); - assert!(get.status.is_none()); - assert!(get.share_url.is_none()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_get_context_attribution_and_heaviest_messages_after_turn() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state_extras", - "should_get_context_attribution_and_heaviest_messages_after_turn", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let answer = session - .send_and_wait("Say CONTEXT_METADATA_OK exactly.") - .await - .expect("send prompt") - .expect("assistant message"); - assert!(assistant_message_content(&answer).contains("CONTEXT_METADATA_OK")); - - let attribution = session - .rpc() - .metadata() - .get_context_attribution() - .await - .expect("get context attribution"); - assert!(attribution.context_attribution.is_some()); - let heaviest = session - .rpc() - .metadata() - .get_context_heaviest_messages(MetadataContextHeaviestMessagesRequest { - limit: Some(5), - }) - .await - .expect("get heaviest messages"); - assert!(heaviest.total_tokens >= 0); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_update_and_clear_live_subagent_settings() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state_extras", - "should_update_and_clear_live_subagent_settings", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - session - .rpc() - .tools() - .update_subagent_settings(UpdateSubagentSettingsRequest { - subagents: Some(UpdateSubagentSettingsRequestSubagents { - agents: Some(HashMap::from([( - "general-purpose".to_string(), - SubagentSettingsEntry { - auto_invoke: None, - context_tier: Some( - SubagentSettingsEntryContextTier::LongContext, - ), - effort_level: Some("low".to_string()), - model: Some("gpt-5-mini".to_string()), - model_policy: None, - }, - )])), - disabled_subagents: Some(vec!["legacy-agent".to_string()]), - max_concurrency: None, - max_depth: None, - }), - }) - .await - .expect("update subagent settings"); - session - .rpc() - .tools() - .update_subagent_settings(UpdateSubagentSettingsRequest { subagents: None }) - .await - .expect("clear subagent settings"); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_reload_session_plugins() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_session_state_extras", - "should_reload_session_plugins", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - session - .rpc() - .plugins() - .reload() - .await - .expect("reload session plugins"); - - let plugins = session - .rpc() - .plugins() - .list() - .await - .expect("list session plugins"); - assert!( - plugins - .plugins - .iter() - .all(|plugin| !plugin.name.trim().is_empty()) - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_session_state_extras", 11); diff --git a/rust/tests/e2e/rpc_shell_and_fleet.rs b/rust/tests/e2e/rpc_shell_and_fleet.rs deleted file mode 100644 index 968d511475..0000000000 --- a/rust/tests/e2e/rpc_shell_and_fleet.rs +++ /dev/null @@ -1,123 +0,0 @@ -use github_copilot_sdk::rpc::{ShellExecRequest, ShellKillRequest}; - -use super::support::wait_for_condition; - -#[tokio::test] -async fn should_execute_shell_command() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_shell_and_fleet", - "should_execute_shell_command", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let marker_path = ctx.work_dir().join("shell-rpc-marker.txt"); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let result = session - .rpc() - .shell() - .exec(ShellExecRequest { - command: write_file_command(&marker_path, "copilot-sdk-shell-rpc"), - cwd: Some(ctx.work_dir().display().to_string()), - timeout: None, - }) - .await - .expect("execute shell command"); - - assert!(!result.process_id.trim().is_empty()); - wait_for_file_text(&marker_path, "copilot-sdk-shell-rpc").await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_kill_shell_process() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_shell_and_fleet", - "should_kill_shell_process", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let exec = session - .rpc() - .shell() - .exec(ShellExecRequest { - command: long_running_command(), - cwd: Some(ctx.work_dir().display().to_string()), - timeout: None, - }) - .await - .expect("start shell process"); - assert!(!exec.process_id.trim().is_empty()); - - let killed = session - .rpc() - .shell() - .kill(ShellKillRequest { - process_id: exec.process_id, - signal: None, - }) - .await - .expect("kill shell process"); - assert!(killed.killed); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -async fn wait_for_file_text(path: &std::path::Path, expected: &'static str) { - wait_for_condition("shell command output file", || async { - match std::fs::read_to_string(path) { - Ok(content) => content.contains(expected), - Err(_) => false, - } - }) - .await; -} - -#[cfg(windows)] -fn write_file_command(path: &std::path::Path, marker: &str) -> String { - format!( - "powershell -NoLogo -NoProfile -Command \"Set-Content -LiteralPath '{}' -Value '{}'\"", - path.display(), - marker - ) -} - -#[cfg(not(windows))] -fn write_file_command(path: &std::path::Path, marker: &str) -> String { - format!("sh -c \"printf '%s' '{}' > '{}'\"", marker, path.display()) -} - -#[cfg(windows)] -fn long_running_command() -> String { - "powershell -NoLogo -NoProfile -Command \"Start-Sleep -Seconds 30\"".to_string() -} - -#[cfg(not(windows))] -fn long_running_command() -> String { - "sleep 30".to_string() -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_shell_and_fleet", 2); diff --git a/rust/tests/e2e/rpc_shell_edge_cases.rs b/rust/tests/e2e/rpc_shell_edge_cases.rs deleted file mode 100644 index 3d1717723a..0000000000 --- a/rust/tests/e2e/rpc_shell_edge_cases.rs +++ /dev/null @@ -1,421 +0,0 @@ -use std::path::Path; -use std::time::Duration; - -use github_copilot_sdk::rpc::{ShellExecRequest, ShellKillRequest, ShellKillSignal}; - -use super::support::wait_for_condition; - -#[tokio::test] -async fn shell_exec_with_timeout_kills_long_running_command() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_shell_edge_cases", - "shell_exec_with_timeout_kills_long_running_command", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let timeout = shell_timeout(); - let started_path = ctx.work_dir().join("shell-timeout-started.txt"); - let marker_path = ctx.work_dir().join("shell-timeout-marker.txt"); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let result = session - .rpc() - .shell() - .exec(ShellExecRequest { - command: delayed_write_command(&started_path, &marker_path), - cwd: Some(ctx.work_dir().display().to_string()), - timeout: Some( - timeout - .as_millis() - .try_into() - .expect("shell timeout fits in i64"), - ), - }) - .await - .expect("execute timed command"); - assert!(!result.process_id.trim().is_empty()); - - wait_for_exists(&started_path).await; - // The cleanup probe should not terminate a process before its timeout expires. - tokio::time::sleep(timeout).await; - wait_for_process_cleanup(&session, result.process_id, "timed-out command").await; - assert!( - !marker_path.exists(), - "timeout should kill before marker is written" - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn shell_exec_with_custom_cwd_honors_override() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_shell_edge_cases", - "shell_exec_with_custom_cwd_honors_override", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let subdir = ctx.work_dir().join("shell-cwd"); - std::fs::create_dir_all(&subdir).expect("create shell cwd"); - let marker_path = subdir.join("marker.txt"); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let result = session - .rpc() - .shell() - .exec(ShellExecRequest { - command: write_relative_marker_command("shell-cwd-marker"), - cwd: Some(subdir.display().to_string()), - timeout: None, - }) - .await - .expect("execute cwd command"); - - assert!(!result.process_id.trim().is_empty()); - wait_for_file_text(&marker_path, "shell-cwd-marker").await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn shell_exec_with_nonexistent_command_returns_processid_and_cleans_up() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_shell_edge_cases", - "shell_exec_with_nonexistent_command_returns_processid_and_cleans_up", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let result = session - .rpc() - .shell() - .exec(ShellExecRequest { - command: nonexistent_command(), - cwd: Some(ctx.work_dir().display().to_string()), - timeout: None, - }) - .await - .expect("execute nonexistent command"); - - assert!(!result.process_id.trim().is_empty()); - wait_for_process_cleanup(&session, result.process_id, "nonexistent command").await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn shell_kill_unknown_processid_returns_false() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_shell_edge_cases", - "shell_kill_unknown_processid_returns_false", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let result = session - .rpc() - .shell() - .kill(ShellKillRequest { - process_id: "unknown-rust-process".to_string(), - signal: None, - }) - .await - .expect("kill unknown process"); - - assert!(!result.killed); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn shell_kill_cleans_up_after_terminating_signal() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_shell_edge_cases", - "shell_kill_cleans_up_after_terminating_signal", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let exec = session - .rpc() - .shell() - .exec(ShellExecRequest { - command: long_running_command(), - cwd: Some(ctx.work_dir().display().to_string()), - timeout: None, - }) - .await - .expect("start shell"); - - let killed = session - .rpc() - .shell() - .kill(ShellKillRequest { - process_id: exec.process_id.clone(), - signal: Some(ShellKillSignal::SIGTERM), - }) - .await - .expect("kill shell"); - assert!(killed.killed); - wait_for_process_cleanup(&session, exec.process_id, "killed command").await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn shell_exec_with_stderr_output_cleans_up() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_shell_edge_cases", - "shell_exec_with_stderr_output_cleans_up", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let marker_path = ctx.work_dir().join("shell-stderr-marker.txt"); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let result = session - .rpc() - .shell() - .exec(ShellExecRequest { - command: stderr_command(&marker_path), - cwd: Some(ctx.work_dir().display().to_string()), - timeout: None, - }) - .await - .expect("execute stderr command"); - - wait_for_exists(&marker_path).await; - wait_for_process_cleanup(&session, result.process_id, "stderr command").await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn shell_exec_with_large_stdout_cleans_up() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_shell_edge_cases", - "shell_exec_with_large_stdout_cleans_up", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let marker_path = ctx.work_dir().join("shell-stdout-marker.txt"); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let result = session - .rpc() - .shell() - .exec(ShellExecRequest { - command: large_stdout_command(&marker_path), - cwd: Some(ctx.work_dir().display().to_string()), - timeout: None, - }) - .await - .expect("execute large stdout command"); - - wait_for_exists(&marker_path).await; - wait_for_process_cleanup(&session, result.process_id, "large stdout command").await; - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -async fn wait_for_exists(path: &Path) { - wait_for_condition("shell marker file", || async { path.exists() }).await; -} - -async fn wait_for_file_text(path: &Path, expected: &'static str) { - wait_for_condition("shell marker text", || async { - match std::fs::read_to_string(path) { - Ok(content) => content.contains(expected), - Err(_) => false, - } - }) - .await; -} - -async fn wait_for_process_cleanup( - session: &github_copilot_sdk::session::Session, - process_id: String, - scenario: &'static str, -) { - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - let result = session - .rpc() - .shell() - .kill(ShellKillRequest { - process_id, - signal: None, - }) - .await - .expect("probe process cleanup"); - assert!( - !result.killed, - "{scenario} should have already exited and been removed from the runtime process map" - ); -} - -#[cfg(windows)] -fn delayed_write_command(started_path: &Path, marker_path: &Path) -> String { - format!( - "powershell -NoLogo -NoProfile -Command \"Set-Content -LiteralPath '{}' -Value started; Start-Sleep -Seconds 30; Set-Content -LiteralPath '{}' -Value should-not-exist\"", - started_path.display(), - marker_path.display() - ) -} - -#[cfg(windows)] -fn shell_timeout() -> Duration { - Duration::from_secs(2) -} - -#[cfg(not(windows))] -fn delayed_write_command(started_path: &Path, marker_path: &Path) -> String { - format!( - "sh -c \"printf started > '{}'; sleep 30; printf should-not-exist > '{}'\"", - started_path.display(), - marker_path.display() - ) -} - -#[cfg(not(windows))] -fn shell_timeout() -> Duration { - Duration::from_millis(200) -} - -#[cfg(windows)] -fn write_relative_marker_command(marker: &str) -> String { - format!( - "powershell -NoLogo -NoProfile -Command \"Set-Content -LiteralPath 'marker.txt' -Value '{marker}'\"" - ) -} - -#[cfg(not(windows))] -fn write_relative_marker_command(marker: &str) -> String { - format!("sh -c \"printf '%s' '{marker}' > marker.txt\"") -} - -#[cfg(windows)] -fn long_running_command() -> String { - "powershell -NoLogo -NoProfile -Command \"Start-Sleep -Seconds 60\"".to_string() -} - -#[cfg(not(windows))] -fn long_running_command() -> String { - "sleep 60".to_string() -} - -#[cfg(windows)] -fn nonexistent_command() -> String { - "cmd /C definitely-not-a-real-command-rust-12345".to_string() -} - -#[cfg(not(windows))] -fn nonexistent_command() -> String { - "sh -c 'definitely-not-a-real-command-rust-12345'".to_string() -} - -#[cfg(windows)] -fn stderr_command(marker_path: &Path) -> String { - format!( - "powershell -NoLogo -NoProfile -Command \"[Console]::Error.WriteLine('boom'); exit 2\" & echo done > \"{}\" & exit /b 2", - marker_path.display() - ) -} - -#[cfg(not(windows))] -fn stderr_command(marker_path: &Path) -> String { - format!( - "sh -c \"echo boom 1>&2; printf done > '{}'; exit 2\"", - marker_path.display() - ) -} - -#[cfg(windows)] -fn large_stdout_command(marker_path: &Path) -> String { - format!( - "powershell -NoLogo -NoProfile -Command \"Write-Host ('x' * 71680); Set-Content -LiteralPath '{}' -Value done\"", - marker_path.display() - ) -} - -#[cfg(not(windows))] -fn large_stdout_command(marker_path: &Path) -> String { - format!( - "sh -c \"python3 - <<'PY'\nprint('x' * 71680)\nPY\nprintf done > '{}'\"", - marker_path.display() - ) -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_shell_edge_cases", 7); diff --git a/rust/tests/e2e/rpc_shell_user_requested.rs b/rust/tests/e2e/rpc_shell_user_requested.rs deleted file mode 100644 index 43de1c2ccd..0000000000 --- a/rust/tests/e2e/rpc_shell_user_requested.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::path::Path; -use std::sync::Arc; -use std::time::Duration; - -use github_copilot_sdk::RequestId; -use github_copilot_sdk::rpc::{ShellCancelUserRequestedRequest, ShellExecuteUserRequestedRequest}; - -use super::support::wait_for_condition; - -#[tokio::test] -async fn should_execute_user_requested_shell_command() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_shell_user_requested", - "should_execute_user_requested_shell_command", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let marker = format!("copilotusershell{}", uuid::Uuid::new_v4().simple()); - let request_id = RequestId::new(format!("req-{}", uuid::Uuid::new_v4().simple())); - - let result = session - .rpc() - .shell() - .execute_user_requested(ShellExecuteUserRequestedRequest { - request_id, - command: format!("echo {marker}"), - }) - .await - .expect("execute user-requested shell command"); - - assert!( - result.success, - "expected shell command to succeed: {:?}", - result.error - ); - assert_eq!(result.exit_code, Some(0)); - assert!(result.output.contains(&marker)); - assert!(!result.tool_call_id.trim().is_empty()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_cancel_user_requested_shell_command() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_shell_user_requested", - "should_cancel_user_requested_shell_command", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = Arc::new( - client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"), - ); - - let missing = session - .rpc() - .shell() - .cancel_user_requested(ShellCancelUserRequestedRequest { - request_id: RequestId::new(format!( - "missing-{}", - uuid::Uuid::new_v4().simple() - )), - }) - .await - .expect("cancel missing request"); - assert!(!missing.cancelled); - - let request_id = RequestId::new(format!("req-{}", uuid::Uuid::new_v4().simple())); - let marker_dir = tempfile::Builder::new() - .prefix("shell-cancel-") - .tempdir() - .expect("create shell cancel marker directory"); - let marker_path = marker_dir.path().join("marker.txt"); - let command = create_marker_then_sleep_command(&marker_path, 60); - let execute_session = Arc::clone(&session); - let execute_request_id = request_id.clone(); - let mut execute_task = tokio::spawn(async move { - execute_session - .rpc() - .shell() - .execute_user_requested(ShellExecuteUserRequestedRequest { - request_id: execute_request_id, - command, - }) - .await - }); - - wait_for_file_text(&marker_path, "running").await; - wait_for_condition("user-requested shell command cancellation", || { - let session = Arc::clone(&session); - let request_id = request_id.clone(); - async move { - session - .rpc() - .shell() - .cancel_user_requested(ShellCancelUserRequestedRequest { request_id }) - .await - .expect("cancel running request") - .cancelled - } - }) - .await; - - // Await the spawned task by mutable reference so a timeout can abort it instead of - // dropping the handle. A dropped JoinHandle detaches the task, leaving the shell - // command running in the background where it can keep file handles open and - // destabilize later tests. - let result = - match tokio::time::timeout(Duration::from_secs(30), &mut execute_task).await { - Ok(joined) => joined - .expect("shell execution task should not panic") - .expect("execute user-requested shell command"), - Err(_elapsed) => { - execute_task.abort(); - panic!("cancelled shell command did not finish within 30s"); - } - }; - assert!(!result.success); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -async fn wait_for_file_text(path: &Path, expected: &'static str) { - wait_for_condition("shell marker text", || async { - std::fs::read_to_string(path).is_ok_and(|content| content.contains(expected)) - }) - .await; -} - -#[cfg(windows)] -fn create_marker_then_sleep_command(marker_path: &Path, seconds: u64) -> String { - format!( - "Set-Content -LiteralPath {} -Value 'running'; Start-Sleep -Seconds {seconds}", - powershell_quote(marker_path) - ) -} - -#[cfg(not(windows))] -fn create_marker_then_sleep_command(marker_path: &Path, seconds: u64) -> String { - format!( - "echo running > {}; sleep {seconds}", - posix_shell_quote(marker_path) - ) -} - -#[cfg(windows)] -fn powershell_quote(path: &Path) -> String { - format!("'{}'", path.display().to_string().replace('\'', "''")) -} - -#[cfg(not(windows))] -fn posix_shell_quote(path: &Path) -> String { - format!("'{}'", path.display().to_string().replace('\'', "'\\''")) -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_shell_user_requested", 2); diff --git a/rust/tests/e2e/rpc_smoke.rs b/rust/tests/e2e/rpc_smoke.rs new file mode 100644 index 0000000000..8c5de45034 --- /dev/null +++ b/rust/tests/e2e/rpc_smoke.rs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +use github_copilot_sdk::rpc::PingRequest; +use github_copilot_sdk::session_events::SessionMode; + +static E2E: super::support::SharedE2eGroup = + super::support::SharedE2eGroup::standard("rpc_server", 1); + +#[tokio::test] +async fn should_round_trip_generated_server_and_session_rpc() { + super::support::with_shared_e2e_context( + &E2E, + "rpc_server", + "should_call_rpc_ping_with_typed_params_and_result", + |ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let client = ctx.start_client().await; + let pong = client + .rpc() + .ping(PingRequest { + message: Some("typed rpc test".to_string()), + }) + .await + .expect("ping"); + assert_eq!(pong.message, "pong: typed rpc test"); + assert!(!pong.timestamp.is_empty()); + + let session = client + .create_session(ctx.approve_all_session_config()) + .await + .expect("create session"); + assert_eq!( + session.rpc().mode().get().await.expect("get mode"), + SessionMode::Interactive + ); + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} diff --git a/rust/tests/e2e/rpc_surface_coverage.rs b/rust/tests/e2e/rpc_surface_coverage.rs deleted file mode 100644 index 11e0e2d764..0000000000 --- a/rust/tests/e2e/rpc_surface_coverage.rs +++ /dev/null @@ -1,1496 +0,0 @@ -use std::collections::HashMap; -use std::path::PathBuf; - -use github_copilot_sdk::rpc::*; -use github_copilot_sdk::{CliProgram, Client, ClientOptions, ErrorKind, SessionConfig, Transport}; -use serde::Serialize; -use serde_json::{Value, json}; -use tempfile::TempDir; - -macro_rules! rpc_ok { - ($call:expr) => { - $call - .await - .unwrap_or_else(|error| panic!("{} failed: {error}", stringify!($call))) - }; -} - -#[tokio::test] -async fn client_rpc_surface_uses_typed_namespaces_and_round_trips_results() { - let mut results = ResponseMap::default(); - results.insert( - "account.getQuota", - AccountGetQuotaResult { - quota_snapshots: HashMap::from([( - "premium_interactions".to_string(), - AccountQuotaSnapshot { - entitlement_requests: 300, - remaining_percentage: 62.5, - reset_date: Some("2026-10-01T00:00:00Z".to_string()), - used_requests: 112, - ..Default::default() - }, - )]), - }, - ); - results.insert( - "catalog.search", - CatalogSearchResult::Succeeded(CatalogSearchSucceeded { - search_id: "search-17".to_string(), - truncated: true, - ..Default::default() - }), - ); - results.insert_default::("extensions.discover"); - results.insert_default::("hooks.discover"); - results.insert_default::("llmInference.setProvider"); - results.insert_default::("managedSettings.read"); - results.insert( - "mcp.planInstall", - McpPlanInstallResult::Unavailable(CatalogUnavailableError { - message: "catalog intentionally offline".to_string(), - ..Default::default() - }), - ); - results.insert_default::("models.list"); - results.insert_default::("models.getBuiltInCatalog"); - results.insert_default::("plugins.marketplaces.refresh"); - results.insert_value( - "sessions.getClientMetadata", - json!([{"clientName": "fixture-host", "capabilities": ["rpc"]}]), - ); - results.insert_default::("sessions.readPersistedEvents"); - - let fake = FakeCli::new(results, ErrorMap::default()); - let client = fake.start_client().await; - - rpc_ok!(client.rpc().register_extension_launch_provider()); - let quota = rpc_ok!( - client - .rpc() - .account() - .get_quota_with_params(AccountGetQuotaRequest { - git_hub_token: Some("quota-token".to_string()), - selection_id: Some("account-42".to_string()), - }) - ); - let snapshot = quota - .quota_snapshots - .get("premium_interactions") - .expect("premium quota snapshot"); - assert_eq!(snapshot.entitlement_requests, 300); - assert_eq!(snapshot.used_requests, 112); - assert_eq!(snapshot.remaining_percentage, 62.5); - assert_eq!(snapshot.reset_date.as_deref(), Some("2026-10-01T00:00:00Z")); - - let search = rpc_ok!(client.rpc().catalog().search(CatalogSearchRequest { - contract: CatalogClientContract { - protocol_version: 7, - required_capabilities: vec!["install-plans".to_string()], - }, - kinds: None, - limit: Some(4), - page: None, - query: "offline catalog".to_string(), - })); - let CatalogSearchResult::Succeeded(search) = search else { - panic!("expected successful catalog search"); - }; - assert_eq!(search.search_id, "search-17"); - assert!(search.truncated); - - rpc_ok!(client.rpc().extensions().discover()); - rpc_ok!(client.rpc().hooks().discover(HooksDiscoverRequest { - exclude_host_hooks: Some(true), - project_paths: Some(vec!["project-a".to_string(), "project-b".to_string()]), - })); - rpc_ok!(client.rpc().llm_inference().set_provider()); - rpc_ok!(client.rpc().managed_settings().read()); - - let plan = rpc_ok!(client.rpc().mcp().plan_install(McpPlanInstallRequest { - contract: CatalogClientContract { - protocol_version: 7, - required_capabilities: vec!["install-plans".to_string()], - }, - scope: None, - source: McpPlanInstallSource::Candidate(McpPlanInstallSourceCandidate { - candidate_handle: "candidate-handle".to_string(), - kind: McpPlanInstallSourceCandidateKind::Candidate, - search_id: "search-17".to_string(), - }), - })); - let McpPlanInstallResult::Unavailable(unavailable) = plan else { - panic!("expected typed unavailable install plan"); - }; - assert_eq!(unavailable.message, "catalog intentionally offline"); - - rpc_ok!( - client - .rpc() - .models() - .list_with_params(ModelsListRequest::default()) - ); - rpc_ok!(client.rpc().models().get_built_in_catalog()); - rpc_ok!( - client - .rpc() - .plugins() - .builtin() - .set(PluginsBuiltinSetRequest::default()) - ); - rpc_ok!(client.rpc().plugins().marketplaces().refresh()); - let metadata = rpc_ok!( - client - .rpc() - .sessions() - .get_client_metadata(SessionsGetClientMetadataRequest::default()) - ); - assert_eq!(metadata[0]["clientName"], "fixture-host"); - assert_eq!(metadata[0]["capabilities"][0], "rpc"); - rpc_ok!( - client - .rpc() - .sessions() - .read_persisted_events(SessionsReadPersistedEventsRequest::default()) - ); - rpc_ok!( - client - .rpc() - .skills() - .config() - .set_skill_disabled(SkillsConfigSetSkillDisabledRequest::default()) - ); - - client.stop().await.expect("stop fake CLI"); - - fake.assert_target_methods(&[ - "registerExtensionLaunchProvider", - "account.getQuota", - "catalog.search", - "extensions.discover", - "hooks.discover", - "llmInference.setProvider", - "managedSettings.read", - "mcp.planInstall", - "models.list", - "models.getBuiltInCatalog", - "plugins.builtin.set", - "plugins.marketplaces.refresh", - "sessions.getClientMetadata", - "sessions.readPersistedEvents", - "skills.config.setSkillDisabled", - ]); - fake.assert_params( - "account.getQuota", - 0, - json!({ - "gitHubToken": "quota-token", - "selectionId": "account-42" - }), - ); - fake.assert_params( - "catalog.search", - 0, - json!({ - "contract": { - "protocolVersion": 7, - "requiredCapabilities": ["install-plans"] - }, - "limit": 4, - "query": "offline catalog" - }), - ); - fake.assert_params( - "mcp.planInstall", - 0, - json!({ - "contract": { - "protocolVersion": 7, - "requiredCapabilities": ["install-plans"] - }, - "source": { - "candidateHandle": "candidate-handle", - "kind": "candidate", - "searchId": "search-17" - } - }), - ); -} - -#[tokio::test] -async fn session_lifecycle_workflow_and_history_rpc_surface_is_typed() { - let mut results = ResponseMap::default(); - results.insert_default::("session.sendMessages"); - results.insert_default::("session.abort"); - results.insert_default::("session.interruptMainTurn"); - results.insert_value("session.cancelAllBackgroundAgents", json!(3)); - results.insert_default::("session.agent.list"); - results.insert( - "session.autopilotObjective.getState", - AutopilotObjectiveGetStateResult { - state: Some(AutopilotObjectiveState::default()), - }, - ); - results.insert_default::( - "session.completions.getTriggerCharacters", - ); - results - .insert_default::("session.contentExclusion.checkPaths"); - results.insert( - "session.workflow.run", - WorkflowRunResult { - attempt: Some(2), - result: Some(json!({"artifact": "workflow-output"})), - run_id: "run-123".to_string(), - ..Default::default() - }, - ); - results.insert( - "session.workflow.resume", - WorkflowResumeResult { - workflow_name: "coverage-workflow".to_string(), - run: WorkflowRunResult { - run_id: "run-123".to_string(), - ..Default::default() - }, - }, - ); - results.insert_default::("session.workflow.getRun"); - results.insert_default::("session.workflow.listRuns"); - results.insert_default::("session.workflow.getRunDetail"); - results.insert_default::("session.workflow.getRunProgress"); - results.insert_default::("session.workflow.cancel"); - results.insert_default::("session.workflow.pause"); - results.insert_default::("session.workflow.log"); - results.insert_default::("session.workflow.agent"); - results.insert_default::("session.workflow.journal.get"); - results.insert_default::("session.workflow.journal.put"); - results.insert_default::("session.fleet.start"); - results.insert( - "session.history.compact", - HistoryCompactResult { - messages_removed: 8, - success: true, - summary_content: Some("deterministic summary".to_string()), - tokens_removed: 144, - ..Default::default() - }, - ); - results.insert( - "session.history.clearContext", - HistoryClearContextResult { - messages_cleared: 5, - }, - ); - results.insert( - "session.limitPrediction.predict", - SessionLimitPredictionResult::Unavailable(SessionLimitPredictionResultUnavailable { - reason: SessionLimitPredictionUnavailableReason::NoModel, - ..Default::default() - }), - ); - - let fake = FakeCli::new(results, ErrorMap::default()); - let client = fake.start_client().await; - let session = fake.create_session(&client).await; - - rpc_ok!(session.rpc().suspend()); - rpc_ok!(session.rpc().send_messages(SendMessagesRequest::default())); - rpc_ok!(session.rpc().abort(AbortRequest::default())); - rpc_ok!( - session - .rpc() - .interrupt_main_turn(InterruptMainTurnRequest::default()) - ); - let cancelled = rpc_ok!(session.rpc().cancel_all_background_agents()); - assert_eq!(cancelled, 3); - rpc_ok!( - session - .rpc() - .agent() - .list_with_params(AgentListRequest::default()) - ); - rpc_ok!( - session - .rpc() - .agent() - .set_prompt(AgentSetPromptRequest::default()) - ); - let objective = rpc_ok!(session.rpc().autopilot_objective().get_state()); - assert!(objective.state.is_some()); - rpc_ok!(session.rpc().completions().get_trigger_characters()); - rpc_ok!( - session - .rpc() - .content_exclusion() - .check_paths(ContentExclusionCheckPathsRequest { - paths: vec![ - "C:\\workspace\\one.rs".to_string(), - "/workspace/two.rs".to_string() - ], - }) - ); - - let run = rpc_ok!(session.rpc().workflow().run(WorkflowRunRequest { - args: json!({"mode": "offline", "count": 2}), - name: "coverage-workflow".to_string(), - options: None, - })); - assert_eq!(run.run_id, "run-123"); - assert_eq!(run.attempt, Some(2)); - assert_eq!(run.result, Some(json!({"artifact": "workflow-output"}))); - let resumed = rpc_ok!(session.rpc().workflow().resume(WorkflowResumeRequest { - run_id: "run-123".to_string(), - notify_on_complete: Some(false), - ..Default::default() - })); - assert_eq!(resumed.workflow_name, "coverage-workflow"); - assert_eq!(resumed.run.run_id, "run-123"); - rpc_ok!( - session - .rpc() - .workflow() - .get_run(WorkflowGetRunRequest::default()) - ); - rpc_ok!( - session - .rpc() - .workflow() - .list_runs(WorkflowListRunsRequest::default()) - ); - rpc_ok!( - session - .rpc() - .workflow() - .get_run_detail(WorkflowGetRunRequest::default()) - ); - rpc_ok!( - session - .rpc() - .workflow() - .get_run_progress(WorkflowGetRunProgressRequest::default()) - ); - rpc_ok!( - session - .rpc() - .workflow() - .cancel(WorkflowCancelRequest::default()) - ); - rpc_ok!( - session - .rpc() - .workflow() - .pause(WorkflowPauseRequest::default()) - ); - rpc_ok!(session.rpc().workflow().log(WorkflowLogRequest::default())); - rpc_ok!( - session - .rpc() - .workflow() - .agent(WorkflowAgentRequest::default()) - ); - rpc_ok!( - session - .rpc() - .workflow() - .journal() - .get(WorkflowJournalGetRequest::default()) - ); - rpc_ok!( - session - .rpc() - .workflow() - .journal() - .put(WorkflowJournalPutRequest::default()) - ); - rpc_ok!(session.rpc().fleet().start(FleetStartRequest::default())); - - let compact = rpc_ok!(session.rpc().history().compact()); - assert!(compact.success); - assert_eq!(compact.messages_removed, 8); - assert_eq!(compact.tokens_removed, 144); - assert_eq!( - compact.summary_content.as_deref(), - Some("deterministic summary") - ); - rpc_ok!( - session - .rpc() - .history() - .compact_with_params(HistoryCompactRequest::default()) - ); - let cleared = rpc_ok!( - session - .rpc() - .history() - .clear_context(HistoryClearContextRequest::default()) - ); - assert_eq!(cleared.messages_cleared, 5); - let prediction = rpc_ok!(session.rpc().limit_prediction().predict()); - assert!(matches!( - prediction, - SessionLimitPredictionResult::Unavailable(SessionLimitPredictionResultUnavailable { - reason: SessionLimitPredictionUnavailableReason::NoModel, - .. - }) - )); - rpc_ok!( - session - .rpc() - .limit_prediction() - .predict_with_params(SessionLimitPredictionRequest { - model_id: Some("fixture-model".to_string()), - ..Default::default() - }) - ); - - session.disconnect().await.expect("disconnect fake session"); - client.stop().await.expect("stop fake CLI"); - - fake.assert_target_methods(&[ - "session.suspend", - "session.sendMessages", - "session.abort", - "session.interruptMainTurn", - "session.cancelAllBackgroundAgents", - "session.agent.list", - "session.agent.setPrompt", - "session.autopilotObjective.getState", - "session.completions.getTriggerCharacters", - "session.contentExclusion.checkPaths", - "session.workflow.run", - "session.workflow.resume", - "session.workflow.getRun", - "session.workflow.listRuns", - "session.workflow.getRunDetail", - "session.workflow.getRunProgress", - "session.workflow.cancel", - "session.workflow.pause", - "session.workflow.log", - "session.workflow.agent", - "session.workflow.journal.get", - "session.workflow.journal.put", - "session.fleet.start", - "session.history.compact", - "session.history.compact", - "session.history.clearContext", - "session.limitPrediction.predict", - "session.limitPrediction.predict", - ]); - fake.assert_params( - "session.workflow.run", - 0, - json!({ - "sessionId": "rpc-surface-session", - "args": {"mode": "offline", "count": 2}, - "name": "coverage-workflow" - }), - ); - fake.assert_params( - "session.limitPrediction.predict", - 1, - json!({ - "sessionId": "rpc-surface-session", - "modelId": "fixture-model" - }), - ); -} - -#[tokio::test] -async fn session_mcp_metadata_model_and_permission_rpc_surface_is_typed() { - let mut results = ResponseMap::default(); - results - .insert_default::("session.mcp.moveLoadingToBackground"); - results.insert_default::("session.mcp.apps.readResource"); - results.insert( - "session.mcp.oauth.probe", - McpOauthProbeResult::Failed(McpOauthProbeResultFailed { - error: "offline probe fixture".to_string(), - ..Default::default() - }), - ); - results.insert( - "session.mcp.oauth.respond", - McpOauthRespondResult { success: true }, - ); - results.insert_default::("session.mcp.resources.list"); - results - .insert_default::("session.mcp.resources.listTemplates"); - results.insert_value( - "session.metadata.getClientMetadata", - json!({"clientName": "rust-fixture", "version": "1"}), - ); - results.insert_value( - "session.metadata.updateClientMetadata", - json!({"clientName": "updated-fixture", "version": "2"}), - ); - results.insert_default::("session.model.switchAutoTier"); - results.insert_default::("session.model.list"); - results.insert_default::("session.permissions.configure"); - results.insert_default::("session.permissions.pendingRequests"); - results.insert_default::("session.permissions.modifyRules"); - results.insert_default::("session.permissions.setRequired"); - results.insert_default::( - "session.permissions.notifyPromptShown", - ); - results.insert_default::("session.permissions.folderTrust.isTrusted"); - results.insert_default::( - "session.permissions.folderTrust.addTrusted", - ); - results - .insert_default::("session.permissions.locations.resolve"); - results.insert( - "session.permissions.locations.apply", - PermissionLocationApplyResult { - applied_directory_count: 2, - applied_rule_count: 3, - changed: true, - location_key: "repo-key".to_string(), - location_type: PermissionLocationType::Repo, - ..Default::default() - }, - ); - results.insert_default::( - "session.permissions.locations.addToolApproval", - ); - results.insert_default::("session.permissions.paths.list"); - results.insert_default::("session.permissions.paths.add"); - results.insert_default::( - "session.permissions.paths.updatePrimary", - ); - results.insert_default::( - "session.permissions.paths.isPathWithinAllowedDirectories", - ); - results.insert_default::( - "session.permissions.paths.isPathWithinWorkspace", - ); - results.insert_default::( - "session.permissions.urls.setUnrestrictedMode", - ); - results.insert_default::("session.customizations.reload"); - results.insert( - "session.provider.getEndpoint", - ProviderEndpoint { - api_key: Some("fixture-api-key".to_string()), - base_url: "https://offline.invalid/v1".to_string(), - headers: HashMap::from([("x-fixture".to_string(), "rust".to_string())]), - ..Default::default() - }, - ); - - let mut errors = ErrorMap::default(); - errors.insert( - "session.debug.collectLogs", - -32077, - "fixture diagnostics unavailable", - json!({"retryable": false, "source": "offline"}), - ); - let fake = FakeCli::new(results, errors); - let client = fake.start_client().await; - let session = fake.create_session(&client).await; - - let error = session - .rpc() - .debug() - .collect_logs(DebugCollectLogsRequest { - additional_entries: None, - destination: DebugCollectLogsDestination::Directory( - DebugCollectLogsDestinationDirectory { - output_directory: "fixture-debug-output".to_string(), - ..Default::default() - }, - ), - include: None, - }) - .await - .expect_err("debug collection should return fixture RPC error"); - assert_eq!(error.rpc_code(), Some(-32077)); - assert_eq!(error.kind(), &ErrorKind::Rpc { code: -32077 }); - assert!( - error - .to_string() - .contains("fixture diagnostics unavailable") - ); - - rpc_ok!(session.rpc().mcp().move_loading_to_background()); - rpc_ok!( - session - .rpc() - .mcp() - .start_server(McpStartServerRequest::default()) - ); - rpc_ok!( - session - .rpc() - .mcp() - .restart_server(McpRestartServerRequest::default()) - ); - rpc_ok!( - session - .rpc() - .mcp() - .apps() - .read_resource(McpAppsReadResourceRequest::default()) - ); - rpc_ok!( - session - .rpc() - .mcp() - .oauth() - .authentication_state_changed(McpOauthAuthenticationStateChangedRequest::default()) - ); - let probe = rpc_ok!( - session - .rpc() - .mcp() - .oauth() - .probe(McpOauthProbeRequest::default()) - ); - let McpOauthProbeResult::Failed(failed) = probe else { - panic!("expected typed failed OAuth probe"); - }; - assert_eq!(failed.error, "offline probe fixture"); - let responded = rpc_ok!( - session - .rpc() - .mcp() - .oauth() - .respond(McpOauthRespondRequest::default()) - ); - assert!(responded.success); - rpc_ok!( - session - .rpc() - .mcp() - .resources() - .list(McpResourcesListRequest::default()) - ); - rpc_ok!( - session - .rpc() - .mcp() - .resources() - .list_templates(McpResourcesListTemplatesRequest::default()) - ); - - let metadata = rpc_ok!(session.rpc().metadata().get_client_metadata()); - assert_eq!(metadata["clientName"], "rust-fixture"); - let updated = rpc_ok!( - session - .rpc() - .metadata() - .update_client_metadata(MetadataUpdateClientMetadataRequest::default()) - ); - assert_eq!(updated["clientName"], "updated-fixture"); - rpc_ok!( - session - .rpc() - .model() - .switch_auto_tier(ModelSwitchAutoTierRequest::default()) - ); - rpc_ok!( - session - .rpc() - .model() - .list_with_params(ModelListRequest::default()) - ); - - rpc_ok!( - session - .rpc() - .permissions() - .configure(PermissionsConfigureParams::default()) - ); - rpc_ok!(session.rpc().permissions().pending_requests()); - rpc_ok!( - session - .rpc() - .permissions() - .modify_rules(PermissionsModifyRulesParams::default()) - ); - rpc_ok!( - session - .rpc() - .permissions() - .set_required(PermissionsSetRequiredRequest::default()) - ); - rpc_ok!( - session - .rpc() - .permissions() - .notify_prompt_shown(PermissionPromptShownNotification::default()) - ); - rpc_ok!( - session - .rpc() - .permissions() - .folder_trust() - .is_trusted(FolderTrustCheckParams::default()) - ); - rpc_ok!( - session - .rpc() - .permissions() - .folder_trust() - .add_trusted(FolderTrustAddParams::default()) - ); - rpc_ok!( - session - .rpc() - .permissions() - .locations() - .resolve(PermissionLocationResolveParams { - working_directory: "fixture-worktree".to_string(), - }) - ); - let applied = rpc_ok!(session.rpc().permissions().locations().apply( - PermissionLocationApplyParams { - working_directory: "fixture-worktree".to_string(), - } - )); - assert!(applied.changed); - assert_eq!(applied.applied_directory_count, 2); - assert_eq!(applied.applied_rule_count, 3); - assert_eq!(applied.location_key, "repo-key"); - assert_eq!(applied.location_type, PermissionLocationType::Repo); - rpc_ok!(session.rpc().permissions().locations().add_tool_approval( - PermissionLocationAddToolApprovalParams { - approval: PermissionsLocationsAddToolApprovalDetails::Read( - PermissionsLocationsAddToolApprovalDetailsRead::default(), - ), - location_key: "repo-key".to_string(), - } - )); - rpc_ok!(session.rpc().permissions().paths().list()); - rpc_ok!( - session - .rpc() - .permissions() - .paths() - .add(PermissionPathsAddParams { - path: "allowed-dir".to_string(), - }) - ); - rpc_ok!( - session - .rpc() - .permissions() - .paths() - .update_primary(PermissionPathsUpdatePrimaryParams::default()) - ); - rpc_ok!( - session - .rpc() - .permissions() - .paths() - .is_path_within_allowed_directories(PermissionPathsAllowedCheckParams { - path: "allowed-dir/file.rs".to_string(), - }) - ); - rpc_ok!( - session - .rpc() - .permissions() - .paths() - .is_path_within_workspace(PermissionPathsWorkspaceCheckParams::default()) - ); - rpc_ok!( - session - .rpc() - .permissions() - .urls() - .set_unrestricted_mode(PermissionUrlsSetUnrestrictedModeParams::default()) - ); - rpc_ok!(session.rpc().instructions().reload()); - rpc_ok!(session.rpc().customizations().reload()); - rpc_ok!( - session - .rpc() - .plugins() - .reload_with_params(PluginsReloadRequest::default()) - ); - let endpoint = rpc_ok!(session.rpc().provider().get_endpoint_with_params( - ProviderGetEndpointRequest { - model_id: Some("fixture-model".to_string()), - } - )); - assert_eq!(endpoint.base_url, "https://offline.invalid/v1"); - assert_eq!(endpoint.api_key.as_deref(), Some("fixture-api-key")); - assert_eq!(endpoint.headers["x-fixture"], "rust"); - - session.disconnect().await.expect("disconnect fake session"); - client.stop().await.expect("stop fake CLI"); - - fake.assert_target_methods(&[ - "session.debug.collectLogs", - "session.mcp.moveLoadingToBackground", - "session.mcp.startServer", - "session.mcp.restartServer", - "session.mcp.apps.readResource", - "session.mcp.oauth.authenticationStateChanged", - "session.mcp.oauth.probe", - "session.mcp.oauth.respond", - "session.mcp.resources.list", - "session.mcp.resources.listTemplates", - "session.metadata.getClientMetadata", - "session.metadata.updateClientMetadata", - "session.model.switchAutoTier", - "session.model.list", - "session.permissions.configure", - "session.permissions.pendingRequests", - "session.permissions.modifyRules", - "session.permissions.setRequired", - "session.permissions.notifyPromptShown", - "session.permissions.folderTrust.isTrusted", - "session.permissions.folderTrust.addTrusted", - "session.permissions.locations.resolve", - "session.permissions.locations.apply", - "session.permissions.locations.addToolApproval", - "session.permissions.paths.list", - "session.permissions.paths.add", - "session.permissions.paths.updatePrimary", - "session.permissions.paths.isPathWithinAllowedDirectories", - "session.permissions.paths.isPathWithinWorkspace", - "session.permissions.urls.setUnrestrictedMode", - "session.instructions.reload", - "session.customizations.reload", - "session.plugins.reload", - "session.provider.getEndpoint", - ]); - fake.assert_params( - "session.permissions.locations.addToolApproval", - 0, - json!({ - "sessionId": "rpc-surface-session", - "approval": {"kind": "read"}, - "locationKey": "repo-key" - }), - ); - fake.assert_params( - "session.provider.getEndpoint", - 0, - json!({ - "sessionId": "rpc-surface-session", - "modelId": "fixture-model" - }), - ); -} - -#[tokio::test] -async fn session_queue_tasks_tools_ui_and_workspace_rpc_surface_is_typed() { - let task = || TaskClientInfo { - execution_mode: TaskClientExecutionMode::Background, - owner: TaskClientOwner { - kind: TaskClientOwnerKind::Sdk, - presence: TaskClientOwnerPresence::Connected, - ..Default::default() - }, - status: TaskClientStatus::Running, - r#type: TaskClientType::Client, - ..Default::default() - }; - let mut results = ResponseMap::default(); - results.insert_default::("session.queue.moveItem"); - results.insert( - "session.queue.insertAt", - QueueInsertAtResult { - id: "queue-item-9".to_string(), - }, - ); - results.insert_default::("session.queue.removeAt"); - results.insert_default::("session.queue.updateText"); - results.insert_default::("session.queue.duplicateAt"); - results.insert( - "session.queue.sendNow", - QueueSendNowResult { steered: true }, - ); - results.insert_default::("session.sandbox.getEnforcementStatus"); - results.insert_default::("session.sandbox.disableForSession"); - results.insert( - "session.tasks.register", - TasksRegisterResult { - created: true, - task: task(), - ..Default::default() - }, - ); - results.insert( - "session.tasks.update", - TasksUpdateResult { - applied: true, - task: task(), - ..Default::default() - }, - ); - results - .insert_default::("session.tools.getBuiltinDescriptors"); - results.insert( - "session.tools.taskCompleteEventData", - TaskCompleteData { - objective_id: Some(41), - success: Some(true), - summary: Some("coverage complete".to_string()), - ..Default::default() - }, - ); - results.insert_default::("session.tools.set"); - results.insert( - "session.ui.elicitation", - UIElicitationResponse { - action: UIElicitationResponseAction::Accept, - content: Some(HashMap::from([( - "answer".to_string(), - json!("deterministic"), - )])), - ..Default::default() - }, - ); - results.insert( - "session.workspaces.updateMetadata", - workspace_result("workspace-updated"), - ); - results.insert( - "session.workspaces.ensure", - workspace_result("workspace-ensured"), - ); - results.insert( - "session.workspaces.statFile", - WorkspacesStatFileResult { - is_file: true, - mtime_ms: 1_234.0, - size: 88.0, - ..Default::default() - }, - ); - results.insert( - "session.workspaces.addSummary", - WorkspacesAddSummaryResult { - summary: Some(json!({"title": "offline summary", "number": 4})), - workspace: Some(json!({"id": "workspace-ensured"})), - }, - ); - results.insert( - "session.workspaces.truncateSummaries", - workspace_result("workspace-truncated"), - ); - results.insert( - "session.workspaces.readAutopilotObjective", - WorkspacesReadAutopilotObjectiveResult { - content: Some("Ship deterministic coverage".to_string()), - }, - ); - results.insert( - "session.workspaces.writeAutopilotObjective", - WorkspacesWriteAutopilotObjectiveResult { - operation: "created".to_string(), - }, - ); - results.insert_default::( - "session.workspaces.deleteAutopilotObjective", - ); - results.insert( - "session.workspaces.autopilotObjectiveExists", - WorkspacesAutopilotObjectiveExistsResult { exists: true }, - ); - - let fake = FakeCli::new(results, ErrorMap::default()); - let client = fake.start_client().await; - let session = fake.create_session(&client).await; - - rpc_ok!(session.rpc().queue().move_item(QueueMoveItemRequest { - id: "queue-item-1".to_string(), - to_position: 2, - })); - let inserted = rpc_ok!(session.rpc().queue().insert_at(QueueInsertAtRequest { - message: QueueInsertMessage { - billable: Some(false), - display_prompt: Some("Fixture display".to_string()), - prompt: "Queue this deterministically".to_string(), - request_headers: Some(HashMap::from([( - "x-test".to_string(), - "rpc-surface".to_string(), - )])), - ..Default::default() - }, - position: 1, - })); - assert_eq!(inserted.id, "queue-item-9"); - rpc_ok!( - session - .rpc() - .queue() - .remove_at(QueueRemoveAtRequest::default()) - ); - rpc_ok!( - session - .rpc() - .queue() - .update_text(QueueUpdateTextRequest::default()) - ); - rpc_ok!( - session - .rpc() - .queue() - .duplicate_at(QueueDuplicateAtRequest::default()) - ); - rpc_ok!( - session - .rpc() - .queue() - .set_drain_paused(QueueSetDrainPausedRequest { paused: true }) - ); - assert!( - rpc_ok!( - session - .rpc() - .queue() - .send_now(QueueSendNowRequest::default()) - ) - .steered - ); - rpc_ok!(session.rpc().sandbox().get_enforcement_status()); - rpc_ok!( - session - .rpc() - .sandbox() - .disable_for_session(SandboxDisableForSessionRequest::default()) - ); - - let registered = rpc_ok!(session.rpc().tasks().register(TasksRegisterRequest { - cancellable: true, - client_task_id: "client-task-7".to_string(), - description: "deterministic external work".to_string(), - display_name: Some("Coverage task".to_string()), - expected_sequence: Some(0), - r#type: TaskClientType::Client, - })); - assert!(registered.created); - let updated = rpc_ok!(session.rpc().tasks().update(TasksUpdateRequest { - id: "task-7".to_string(), - sequence: 1, - update: TaskClientUpdate::Completed(TaskClientUpdateCompleted { - message: Some("done".to_string()), - result: Some(json!({"files": 2})), - ..Default::default() - }), - })); - assert!(updated.applied); - - rpc_ok!( - session - .rpc() - .tools() - .get_builtin_descriptors(ToolsGetBuiltinDescriptorsRequest::default()) - ); - let completed = rpc_ok!( - session - .rpc() - .tools() - .task_complete_event_data(ToolsTaskCompleteEventDataRequest::default()) - ); - assert_eq!(completed.objective_id, Some(41)); - assert_eq!(completed.success, Some(true)); - assert_eq!(completed.summary.as_deref(), Some("coverage complete")); - rpc_ok!(session.rpc().tools().set(ToolsSetRequest::default())); - let elicitation = rpc_ok!( - session - .rpc() - .ui() - .elicitation(UIElicitationRequest::default()) - ); - assert_eq!(elicitation.action, UIElicitationResponseAction::Accept); - assert_eq!( - elicitation - .content - .as_ref() - .and_then(|content| content.get("answer")), - Some(&json!("deterministic")) - ); - - let updated_workspace = rpc_ok!( - session - .rpc() - .workspaces() - .update_metadata(WorkspacesUpdateMetadataRequest::default()) - ); - assert_eq!( - updated_workspace - .workspace - .as_ref() - .expect("updated workspace") - .id, - "workspace-updated" - ); - rpc_ok!( - session - .rpc() - .workspaces() - .ensure(WorkspacesEnsureRequest::default()) - ); - let stat = rpc_ok!( - session - .rpc() - .workspaces() - .stat_file(WorkspacesStatFileRequest::default()) - ); - assert!(stat.is_file); - assert_eq!(stat.size, 88.0); - assert_eq!(stat.mtime_ms, 1_234.0); - rpc_ok!( - session - .rpc() - .workspaces() - .create_directory(WorkspacesCreateDirectoryRequest { - path: "nested/output".to_string(), - recursive: Some(true), - }) - ); - rpc_ok!( - session - .rpc() - .workspaces() - .remove_path(WorkspacesRemovePathRequest::default()) - ); - rpc_ok!( - session - .rpc() - .workspaces() - .rename_path(WorkspacesRenamePathRequest::default()) - ); - let summary = rpc_ok!( - session - .rpc() - .workspaces() - .add_summary(WorkspacesAddSummaryRequest::default()) - ); - assert_eq!(summary.summary.as_ref().expect("summary")["number"], 4); - rpc_ok!( - session - .rpc() - .workspaces() - .truncate_summaries(WorkspacesTruncateSummariesRequest { keep_count: 2 }) - ); - let objective = rpc_ok!(session.rpc().workspaces().read_autopilot_objective()); - assert_eq!( - objective.content.as_deref(), - Some("Ship deterministic coverage") - ); - let write = rpc_ok!(session.rpc().workspaces().write_autopilot_objective( - WorkspacesWriteAutopilotObjectiveRequest { - content: "Updated deterministic objective".to_string(), - } - )); - assert_eq!(write.operation, "created"); - rpc_ok!(session.rpc().workspaces().delete_autopilot_objective()); - assert!(rpc_ok!(session.rpc().workspaces().autopilot_objective_exists()).exists); - - session.disconnect().await.expect("disconnect fake session"); - client.stop().await.expect("stop fake CLI"); - - fake.assert_target_methods(&[ - "session.queue.moveItem", - "session.queue.insertAt", - "session.queue.removeAt", - "session.queue.updateText", - "session.queue.duplicateAt", - "session.queue.setDrainPaused", - "session.queue.sendNow", - "session.sandbox.getEnforcementStatus", - "session.sandbox.disableForSession", - "session.tasks.register", - "session.tasks.update", - "session.tools.getBuiltinDescriptors", - "session.tools.taskCompleteEventData", - "session.tools.set", - "session.ui.elicitation", - "session.workspaces.updateMetadata", - "session.workspaces.ensure", - "session.workspaces.statFile", - "session.workspaces.createDirectory", - "session.workspaces.removePath", - "session.workspaces.renamePath", - "session.workspaces.addSummary", - "session.workspaces.truncateSummaries", - "session.workspaces.readAutopilotObjective", - "session.workspaces.writeAutopilotObjective", - "session.workspaces.deleteAutopilotObjective", - "session.workspaces.autopilotObjectiveExists", - ]); - fake.assert_params( - "session.queue.insertAt", - 0, - json!({ - "sessionId": "rpc-surface-session", - "message": { - "billable": false, - "displayPrompt": "Fixture display", - "prompt": "Queue this deterministically", - "requestHeaders": {"x-test": "rpc-surface"} - }, - "position": 1 - }), - ); - fake.assert_params( - "session.tasks.update", - 0, - json!({ - "sessionId": "rpc-surface-session", - "id": "task-7", - "sequence": 1, - "update": { - "kind": "completed", - "message": "done", - "result": {"files": 2} - } - }), - ); - fake.assert_params( - "session.workspaces.writeAutopilotObjective", - 0, - json!({ - "sessionId": "rpc-surface-session", - "content": "Updated deterministic objective" - }), - ); -} - -fn workspace_result(id: &str) -> WorkspacesGetWorkspaceResult { - WorkspacesGetWorkspaceResult { - path: Some("fixture-workspace".to_string()), - workspace: Some(WorkspacesGetWorkspaceResultWorkspace { - id: id.to_string(), - ..Default::default() - }), - } -} - -#[derive(Default)] -struct ResponseMap(HashMap<&'static str, Value>); - -impl ResponseMap { - fn insert(&mut self, method: &'static str, result: T) { - self.insert_value( - method, - serde_json::to_value(result).expect("serialize fake RPC result"), - ); - } - - fn insert_default(&mut self, method: &'static str) { - self.insert(method, T::default()); - } - - fn insert_value(&mut self, method: &'static str, result: Value) { - self.0.insert(method, result); - } -} - -#[derive(Default)] -struct ErrorMap(HashMap<&'static str, Value>); - -impl ErrorMap { - fn insert(&mut self, method: &'static str, code: i32, message: &str, data: Value) { - self.0.insert( - method, - json!({ - "code": code, - "message": message, - "data": data - }), - ); - } -} - -struct FakeCli { - _dir: TempDir, - script_path: PathBuf, - capture_path: PathBuf, - config_path: PathBuf, - work_dir: PathBuf, -} - -impl FakeCli { - fn new(results: ResponseMap, errors: ErrorMap) -> Self { - let dir = tempfile::tempdir().expect("create fake CLI temp dir"); - let script_path = dir.path().join("fake-rpc-cli.js"); - let capture_path = dir.path().join("captured-requests.json"); - let config_path = dir.path().join("responses.json"); - let work_dir = dir.path().join("cwd"); - std::fs::create_dir(&work_dir).expect("create fake CLI cwd"); - std::fs::write(&script_path, FAKE_STDIO_CLI_SCRIPT).expect("write fake CLI script"); - std::fs::write( - &config_path, - serde_json::to_vec(&json!({ - "results": results.0, - "errors": errors.0, - })) - .expect("serialize fake CLI config"), - ) - .expect("write fake CLI config"); - Self { - _dir: dir, - script_path, - capture_path, - config_path, - work_dir, - } - } - - async fn start_client(&self) -> Client { - Client::start( - ClientOptions::new() - .with_program(CliProgram::Path(PathBuf::from("node"))) - .with_prefix_args([self.script_path.as_os_str().to_owned()]) - .with_cwd(&self.work_dir) - .with_extra_args([ - "--capture-file".to_string(), - self.capture_path.to_string_lossy().into_owned(), - "--response-config".to_string(), - self.config_path.to_string_lossy().into_owned(), - ]) - .with_github_token("offline-rpc-token") - .with_use_logged_in_user(false) - .with_transport(Transport::Stdio), - ) - .await - .expect("start fake CLI client") - } - - async fn create_session(&self, client: &Client) -> github_copilot_sdk::session::Session { - client - .create_session( - SessionConfig::default() - .with_session_id("rpc-surface-session") - .with_working_directory(&self.work_dir), - ) - .await - .expect("create fake session") - } - - fn assert_target_methods(&self, expected: &[&str]) { - let actual: Vec<_> = self - .capture() - .into_iter() - .filter(|request| { - !matches!( - request.method.as_str(), - "connect" | "runtime.shutdown" | "session.create" | "session.detach" - ) - }) - .map(|request| request.method) - .collect(); - assert_eq!(actual, expected); - } - - fn assert_params(&self, method: &str, occurrence: usize, expected: Value) { - let request = self - .capture() - .into_iter() - .filter(|request| request.method == method) - .nth(occurrence) - .unwrap_or_else(|| panic!("missing occurrence {occurrence} of {method}")); - assert_eq!(request.params, expected, "unexpected params for {method}"); - } - - fn capture(&self) -> Vec { - let bytes = std::fs::read(&self.capture_path).expect("read fake CLI capture"); - serde_json::from_slice(&bytes).expect("parse fake CLI capture") - } -} - -#[derive(serde::Deserialize)] -struct CapturedRequest { - method: String, - #[serde(default)] - params: Value, -} - -const FAKE_STDIO_CLI_SCRIPT: &str = r#" -const fs = require("fs"); - -function argument(name) { - const index = process.argv.indexOf(name); - return index >= 0 ? process.argv[index + 1] : undefined; -} - -const captureFile = argument("--capture-file"); -const config = JSON.parse(fs.readFileSync(argument("--response-config"), "utf8")); -const requests = []; - -function saveCapture() { - fs.writeFileSync(captureFile, JSON.stringify(requests)); -} - -saveCapture(); - -let buffer = Buffer.alloc(0); -process.stdin.on("data", chunk => { - buffer = Buffer.concat([buffer, chunk]); - processBuffer(); -}); -process.stdin.resume(); - -function processBuffer() { - while (true) { - const headerEnd = buffer.indexOf("\r\n\r\n"); - if (headerEnd < 0) return; - const header = buffer.subarray(0, headerEnd).toString("utf8"); - const match = /Content-Length:\s*(\d+)/i.exec(header); - if (!match) throw new Error("Missing Content-Length header"); - const length = Number(match[1]); - const bodyStart = headerEnd + 4; - const bodyEnd = bodyStart + length; - if (buffer.length < bodyEnd) return; - const body = buffer.subarray(bodyStart, bodyEnd).toString("utf8"); - buffer = buffer.subarray(bodyEnd); - handleMessage(JSON.parse(body)); - } -} - -function handleMessage(message) { - if (!Object.prototype.hasOwnProperty.call(message, "id")) return; - - requests.push({ method: message.method, params: message.params }); - saveCapture(); - - if (message.method === "connect") { - writeResult(message.id, { ok: true, protocolVersion: 3, version: "offline-fixture" }); - return; - } - if (message.method === "session.create") { - writeResult(message.id, { - sessionId: message.params.sessionId, - workspacePath: null, - capabilities: null, - }); - return; - } - if (message.method === "session.detach") { - writeResult(message.id, { success: true }); - return; - } - if (Object.prototype.hasOwnProperty.call(config.errors, message.method)) { - writeError(message.id, config.errors[message.method]); - return; - } - - const result = Object.prototype.hasOwnProperty.call(config.results, message.method) - ? config.results[message.method] - : {}; - writeResult(message.id, result); -} - -function writeResult(id, result) { - writeMessage({ jsonrpc: "2.0", id, result }); -} - -function writeError(id, error) { - writeMessage({ jsonrpc: "2.0", id, error }); -} - -function writeMessage(message) { - const body = JSON.stringify(message); - process.stdout.write( - "Content-Length: " + Buffer.byteLength(body, "utf8") + "\r\n\r\n" + body, - ); -} -"#; diff --git a/rust/tests/e2e/rpc_tasks_and_handlers.rs b/rust/tests/e2e/rpc_tasks_and_handlers.rs deleted file mode 100644 index 3a66c6ab74..0000000000 --- a/rust/tests/e2e/rpc_tasks_and_handlers.rs +++ /dev/null @@ -1,512 +0,0 @@ -use std::collections::HashMap; - -use github_copilot_sdk::rpc::{ - CommandsHandlePendingCommandRequest, HandlePendingToolCallRequest, - McpHeadersHandlePendingHeadersRefreshRequest, - McpHeadersHandlePendingHeadersRefreshRequestHeaders, - McpHeadersHandlePendingHeadersRefreshRequestHeadersKind, - McpHeadersHandlePendingHeadersRefreshRequestRequest, PermissionDecision, - PermissionDecisionApproveForLocation, PermissionDecisionApproveForLocationApproval, - PermissionDecisionApproveForLocationApprovalCustomTool, - PermissionDecisionApproveForLocationApprovalCustomToolKind, - PermissionDecisionApproveForLocationKind, PermissionDecisionApproveForSession, - PermissionDecisionApproveForSessionApproval, - PermissionDecisionApproveForSessionApprovalCustomTool, - PermissionDecisionApproveForSessionApprovalCustomToolKind, - PermissionDecisionApproveForSessionKind, PermissionDecisionApproveOnce, - PermissionDecisionApproveOnceKind, PermissionDecisionApprovePermanently, - PermissionDecisionApprovePermanentlyKind, PermissionDecisionReject, - PermissionDecisionRejectKind, PermissionDecisionRequest, TasksCancelRequest, - TasksGetProgressRequest, TasksPromoteToBackgroundRequest, TasksRemoveRequest, - TasksSendMessageRequest, TasksStartAgentRequest, UIAutoModeSwitchResponse, - UIElicitationResponse, UIElicitationResponseAction, UIExitPlanModeResponse, - UIHandlePendingAutoModeSwitchRequest, UIHandlePendingElicitationRequest, - UIHandlePendingExitPlanModeRequest, UIHandlePendingSamplingRequest, - UIHandlePendingSessionLimitsExhaustedRequest, UIHandlePendingUserInputRequest, - UISessionLimitsExhaustedResponse, UISessionLimitsExhaustedResponseAction, - UIUnregisterDirectAutoModeSwitchHandlerRequest, UIUserInputResponse, -}; - -#[tokio::test] -async fn should_list_task_state_and_return_false_for_missing_task_operations() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_tasks_and_handlers", - "should_list_task_state_and_return_false_for_missing_task_operations", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let tasks = session.rpc().tasks().list().await.expect("list tasks"); - assert!(tasks.tasks.is_empty()); - session - .rpc() - .tasks() - .refresh() - .await - .expect("refresh tasks"); - session - .rpc() - .tasks() - .wait_for_pending() - .await - .expect("wait for pending tasks"); - assert!( - session - .rpc() - .tasks() - .get_progress(TasksGetProgressRequest { - id: "missing-task".to_string(), - }) - .await - .expect("progress missing") - .progress - .is_null() - ); - assert!( - session - .rpc() - .tasks() - .get_current_promotable() - .await - .expect("current promotable") - .task - .is_none() - ); - assert!( - !session - .rpc() - .tasks() - .promote_to_background(TasksPromoteToBackgroundRequest { - id: "missing-task".to_string(), - }) - .await - .expect("promote missing") - .promoted - ); - assert!( - session - .rpc() - .tasks() - .promote_current_to_background() - .await - .expect("promote current missing") - .task - .is_none() - ); - assert!( - !session - .rpc() - .tasks() - .cancel(TasksCancelRequest { - id: "missing-task".to_string(), - }) - .await - .expect("cancel missing") - .cancelled - ); - assert!( - !session - .rpc() - .tasks() - .remove(TasksRemoveRequest { - id: "missing-task".to_string(), - }) - .await - .expect("remove missing") - .removed - ); - let send = session - .rpc() - .tasks() - .send_message(TasksSendMessageRequest { - id: "missing-task".to_string(), - message: "hello".to_string(), - from_agent_id: None, - }) - .await - .expect("send missing task"); - assert!(!send.sent); - assert!(send.error.is_some()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_implemented_error_for_missing_task_agent_type() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_tasks_and_handlers", - "should_report_implemented_error_for_missing_task_agent_type", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - assert_implemented_error( - session - .rpc() - .tasks() - .start_agent(TasksStartAgentRequest { - agent_type: "missing-agent-type".to_string(), - prompt: "Say hi".to_string(), - name: "sdk-test-task".to_string(), - description: None, - model: None, - }) - .await, - "session.tasks.startAgent", - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_report_implemented_error_for_invalid_task_agent_model() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_tasks_and_handlers", - "should_report_implemented_error_for_invalid_task_agent_model", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - assert_implemented_error( - session - .rpc() - .tasks() - .start_agent(TasksStartAgentRequest { - agent_type: "general-purpose".to_string(), - prompt: "Say hi".to_string(), - name: "sdk-test-task".to_string(), - description: Some("SDK task agent validation".to_string()), - model: Some("not-a-real-model".to_string()), - }) - .await, - "session.tasks.startAgent", - ); - assert!( - session - .rpc() - .tasks() - .list() - .await - .expect("list tasks after invalid start") - .tasks - .is_empty() - ); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_return_expected_results_for_missing_pending_handler_requestids() { - super::support::with_shared_e2e_context(&E2E, - "rpc_tasks_and_handlers", - "should_return_expected_results_for_missing_pending_handler_requestids", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let tool = session - .rpc() - .tools() - .handle_pending_tool_call(HandlePendingToolCallRequest { - request_id: "missing-tool-request".into(), - result: Some(serde_json::json!("tool result")), - error: None, - }) - .await - .expect("handle missing tool"); - assert!(!tool.success); - - let command = session - .rpc() - .commands() - .handle_pending_command(CommandsHandlePendingCommandRequest { - request_id: "missing-command-request".into(), - error: Some("command error".to_string()), - }) - .await - .expect("handle missing command"); - assert!(command.success); - - let elicitation = session - .rpc() - .ui() - .handle_pending_elicitation(UIHandlePendingElicitationRequest { - request_id: "missing-elicitation-request".into(), - result: UIElicitationResponse { - action: UIElicitationResponseAction::Cancel, - content: Default::default(), - meta: None, - }, - }) - .await - .expect("handle missing elicitation"); - assert!(!elicitation.success); - - let user_input = session - .rpc() - .ui() - .handle_pending_user_input(UIHandlePendingUserInputRequest { - request_id: "missing-user-input-request".into(), - response: UIUserInputResponse { - answer: "answer".to_string(), - was_freeform: true, - }, - }) - .await - .expect("handle missing user input"); - assert!(!user_input.success); - - let sampling = session - .rpc() - .ui() - .handle_pending_sampling(UIHandlePendingSamplingRequest { - request_id: "missing-sampling-request".into(), - response: None, - }) - .await - .expect("handle missing sampling"); - assert!(!sampling.success); - - let auto_mode = session - .rpc() - .ui() - .handle_pending_auto_mode_switch(UIHandlePendingAutoModeSwitchRequest { - request_id: "missing-auto-mode-request".into(), - response: UIAutoModeSwitchResponse::No, - }) - .await - .expect("handle missing auto mode switch"); - assert!(!auto_mode.success); - - let exit_plan = session - .rpc() - .ui() - .handle_pending_exit_plan_mode(UIHandlePendingExitPlanModeRequest { - request_id: "missing-exit-plan-request".into(), - response: UIExitPlanModeResponse { - approved: false, - auto_approve_edits: None, - defer_implementation: None, - feedback: Some("not now".to_string()), - selected_action: None, - }, - }) - .await - .expect("handle missing exit plan"); - assert!(!exit_plan.success); - - let session_limits = session - .rpc() - .ui() - .handle_pending_session_limits_exhausted( - UIHandlePendingSessionLimitsExhaustedRequest { - request_id: "missing-session-limits-request".into(), - response: UISessionLimitsExhaustedResponse { - action: UISessionLimitsExhaustedResponseAction::Unset, - additional_ai_credits: None, - max_ai_credits: None, - }, - }, - ) - .await - .expect("handle missing session limits exhausted"); - assert!(!session_limits.success); - - for (request_id, result) in [ - ( - "missing-permission-request", - PermissionDecision::Reject(PermissionDecisionReject { - feedback: Some("not approved".to_string()), - kind: PermissionDecisionRejectKind::Reject, - }), - ), - ( - "missing-approve-once-request", - PermissionDecision::ApproveOnce(PermissionDecisionApproveOnce { - approved_interactively: None, - kind: PermissionDecisionApproveOnceKind::ApproveOnce, - }), - ), - ( - "missing-permanent-permission-request", - PermissionDecision::ApprovePermanently( - PermissionDecisionApprovePermanently { - domain: "example.com".to_string(), - kind: PermissionDecisionApprovePermanentlyKind::ApprovePermanently, - }, - ), - ), - ( - "missing-session-approval-request", - PermissionDecision::ApproveForSession(PermissionDecisionApproveForSession { - approval: Some(PermissionDecisionApproveForSessionApproval::CustomTool( - PermissionDecisionApproveForSessionApprovalCustomTool { - kind: PermissionDecisionApproveForSessionApprovalCustomToolKind::CustomTool, - tool_name: "missing-tool".to_string(), - }, - )), - domain: None, - kind: PermissionDecisionApproveForSessionKind::ApproveForSession, - }), - ), - ( - "missing-location-approval-request", - PermissionDecision::ApproveForLocation(PermissionDecisionApproveForLocation { - approval: PermissionDecisionApproveForLocationApproval::CustomTool( - PermissionDecisionApproveForLocationApprovalCustomTool { - kind: PermissionDecisionApproveForLocationApprovalCustomToolKind::CustomTool, - tool_name: "missing-tool".to_string(), - }, - ), - kind: PermissionDecisionApproveForLocationKind::ApproveForLocation, - location_key: "missing-location".to_string(), - }), - ), - ] { - let permission = session - .rpc() - .permissions() - .handle_pending_permission_request(PermissionDecisionRequest { - decision_context: None, - request_id: request_id.into(), - result, - }) - .await - .expect("handle missing permission"); - assert!(!permission.success, "{request_id} should not be handled"); - } - - let headers_refresh = session - .rpc() - .mcp() - .headers() - .handle_pending_headers_refresh_request( - McpHeadersHandlePendingHeadersRefreshRequestRequest { - request_id: "missing-headers-refresh-request".into(), - result: McpHeadersHandlePendingHeadersRefreshRequest::Headers( - McpHeadersHandlePendingHeadersRefreshRequestHeaders { - headers: HashMap::from([( - "x-refresh".to_string(), - "missing".to_string(), - )]), - kind: McpHeadersHandlePendingHeadersRefreshRequestHeadersKind::Headers, - ttl_ms: None, - }, - ), - }, - ) - .await - .expect("handle missing headers refresh"); - assert!(!headers_refresh.success); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_register_and_unregister_direct_auto_mode_switch_handler() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_tasks_and_handlers", - "should_register_and_unregister_direct_auto_mode_switch_handler", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let missing = session - .rpc() - .ui() - .unregister_direct_auto_mode_switch_handler( - UIUnregisterDirectAutoModeSwitchHandlerRequest { - handle: "missing-handle".to_string(), - }, - ) - .await - .expect("unregister missing handler"); - assert!(!missing.unregistered); - let handle = session - .rpc() - .ui() - .register_direct_auto_mode_switch_handler() - .await - .expect("register handler") - .handle; - assert!(!handle.trim().is_empty()); - let removed = session - .rpc() - .ui() - .unregister_direct_auto_mode_switch_handler( - UIUnregisterDirectAutoModeSwitchHandlerRequest { handle }, - ) - .await - .expect("unregister handler"); - assert!(removed.unregistered); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -fn assert_implemented_error(result: Result, method: &str) { - let err = match result { - Ok(_) => panic!("RPC should fail"), - Err(err) => err, - }; - let message = err.to_string(); - assert!( - !message.contains(&format!("Unhandled method {method}")), - "expected implemented error for {method}, got {message}" - ); -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_tasks_and_handlers", 5); diff --git a/rust/tests/e2e/rpc_ui_ephemeral_query.rs b/rust/tests/e2e/rpc_ui_ephemeral_query.rs deleted file mode 100644 index 5bb09882b0..0000000000 --- a/rust/tests/e2e/rpc_ui_ephemeral_query.rs +++ /dev/null @@ -1,43 +0,0 @@ -use github_copilot_sdk::rpc::UIEphemeralQueryRequest; - -// TODO(cli-1.0.81-2): CLI 1.0.81-5 still fails session.ui.ephemeralQuery against the -// recorded snapshot on macOS ("Failed to get response from the AI model"). Re-enable -// once the runtime fix ships. -#[ignore = "blocked on CLI 1.0.81-5 session.ui.ephemeralQuery regression on macOS"] -#[tokio::test] -async fn should_answer_ephemeral_query() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_ui_ephemeral_query", - "should_answer_ephemeral_query", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let mut request = UIEphemeralQueryRequest::default(); - request.question = - "In one word, what is the primary color of a clear daytime sky?".to_string(); - let result = session - .rpc() - .ui() - .ephemeral_query(request) - .await - .expect("answer ephemeral query"); - - assert!(!result.answer.trim().is_empty()); - assert!(result.answer.to_ascii_lowercase().contains("blue")); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_ui_ephemeral_query", 1); diff --git a/rust/tests/e2e/rpc_workspace_checkpoints.rs b/rust/tests/e2e/rpc_workspace_checkpoints.rs deleted file mode 100644 index b781f61b2f..0000000000 --- a/rust/tests/e2e/rpc_workspace_checkpoints.rs +++ /dev/null @@ -1,387 +0,0 @@ -use std::path::Path; -use std::process::Command; -use std::sync::Arc; - -use github_copilot_sdk::ResumeSessionConfig; -use github_copilot_sdk::handler::ApproveAllHandler; -use github_copilot_sdk::rpc::{ - SessionsSaveRequest, WorkspaceDiffFileChangeType, WorkspaceDiffMode, WorkspacesDiffRequest, - WorkspacesReadCheckpointRequest, WorkspacesReadFileRequest, WorkspacesSaveLargePasteRequest, - WorkspacesWorkspaceDetailsHostType, -}; - -#[tokio::test] -async fn should_list_no_checkpoints_for_fresh_session() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_workspace_checkpoints", - "should_list_no_checkpoints_for_fresh_session", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let checkpoints = session - .rpc() - .workspaces() - .list_checkpoints() - .await - .expect("list checkpoints"); - assert!(checkpoints.checkpoints.is_empty()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_return_null_or_empty_content_for_unknown_checkpoint() { - if super::support::skip_shared_e2e_inprocess( - &E2E, - "readCheckpoint decodes the id as u32 in-process", - ) - .await - { - return; - } - super::support::with_shared_e2e_context( - &E2E, - "rpc_workspace_checkpoints", - "should_return_null_or_empty_content_for_unknown_checkpoint", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let checkpoint = session - .rpc() - .workspaces() - .read_checkpoint(WorkspacesReadCheckpointRequest { number: i64::MAX }) - .await - .expect("read missing checkpoint"); - assert!(checkpoint.content.as_deref().unwrap_or_default().is_empty()); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_return_typed_workspace_diff_result() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_workspace_checkpoints", - "should_return_typed_workspace_diff_result", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - init_git_repository(ctx.work_dir()); - let changed_path = ctx.work_dir().join("rust-workspace-diff.txt"); - std::fs::write(&changed_path, "diff content\n").expect("write diff file"); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - - let diff = session - .rpc() - .workspaces() - .diff(WorkspacesDiffRequest { - mode: WorkspaceDiffMode::Unstaged, - ..Default::default() - }) - .await - .expect("workspace diff"); - assert_eq!(diff.requested_mode, WorkspaceDiffMode::Unstaged); - assert!(matches!( - diff.mode, - WorkspaceDiffMode::Unstaged | WorkspaceDiffMode::Branch - )); - if let Some(change) = diff.changes.iter().find(|change| { - normalize_path(&change.path).ends_with("rust-workspace-diff.txt") - }) { - assert_eq!(change.change_type, WorkspaceDiffFileChangeType::Added); - assert!(change.diff.contains("diff content") || change.diff.is_empty()); - } else { - assert!( - diff.changes.is_empty(), - "unexpected diff changes: {:?}", - diff.changes - ); - } - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -#[tokio::test] -async fn should_save_large_paste_and_expose_readable_content() { - super::support::with_shared_e2e_context( - &E2E, - "rpc_workspace_checkpoints", - "should_save_large_paste_and_expose_readable_content", - |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - let client = ctx.start_client().await; - let session = client - .create_session(ctx.approve_all_session_config()) - .await - .expect("create session"); - let content = "large paste rust content\n".repeat(512); - - let saved = session - .rpc() - .workspaces() - .save_large_paste(WorkspacesSaveLargePasteRequest { - content: content.clone(), - }) - .await - .expect("save large paste") - .saved - .expect("saved paste descriptor"); - assert!(saved.filename.ends_with(".txt")); - assert_eq!(saved.size_bytes, content.len() as i64); - assert_eq!( - std::fs::read_to_string(&saved.file_path).expect("read saved paste"), - content - ); - let read = session - .rpc() - .workspaces() - .read_file(WorkspacesReadFileRequest { - path: saved.filename, - }) - .await - .expect("read saved paste through workspace"); - assert_eq!(read.content, content); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }, - ) - .await; -} - -fn normalize_path(path: &str) -> String { - path.replace('\\', "/") -} - -#[tokio::test] -async fn should_record_git_context_in_a_new_session_workspace() { - // Reuse session.rs::should_have_stateful_conversation's cassette to create persisted history; - // keep the first-turn prompt below aligned with that owner. - super::support::with_e2e_context("session", "should_have_stateful_conversation", |ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - init_git_repository(ctx.work_dir()); - run_git( - ctx.work_dir(), - &[ - "remote", - "add", - "origin", - "https://github.com/test-org/test-repo.git", - ], - ); - run_git( - ctx.work_dir(), - &["commit", "--quiet", "--allow-empty", "-m", "initial"], - ); - run_git( - ctx.work_dir(), - &["checkout", "--quiet", "-b", "feature/test-branch"], - ); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_client_name("rust-e2e-client"), - ) - .await - .expect("create session"); - - let workspace = session - .rpc() - .workspaces() - .get_workspace() - .await - .expect("get workspace") - .workspace - .expect("workspace"); - - assert_eq!(workspace.repository.as_deref(), Some("test-org/test-repo")); - assert_eq!(workspace.branch.as_deref(), Some("feature/test-branch")); - // Git may expand Windows 8.3 paths or resolve platform directory aliases. - // Compare directory identities, not the spellings returned by Git and the host. - let work_dir = ctx.work_dir().canonicalize().expect("canonical work dir"); - let git_root = Path::new(workspace.git_root.as_deref().expect("git root")) - .canonicalize() - .expect("canonical git root"); - assert_eq!(git_root, work_dir); - assert_eq!( - Path::new(workspace.cwd.as_deref().expect("workspace cwd")) - .canonicalize() - .expect("canonical workspace cwd"), - work_dir - ); - assert_eq!( - workspace.host_type, - Some(WorkspacesWorkspaceDetailsHostType::GitHub) - ); - assert_eq!(workspace.client_name.as_deref(), Some("rust-e2e-client")); - - // The recorded context survives a resume rather than being dropped - // or re-derived into something else. - let session_id = session.id().clone(); - // Empty sessions are not persisted; complete a replay-backed turn before resuming. - let answer = session - .send_and_wait("What is 1+1?") - .await - .expect("send") - .expect("assistant message"); - // Validate the final assistant response arrived (guards against truncated captures). - assert!(super::support::assistant_message_content(&answer).contains('2')); - client - .rpc() - .sessions() - .save(SessionsSaveRequest { - session_id: session_id.clone(), - }) - .await - .expect("persist session before disconnect"); - session.disconnect().await.expect("disconnect session"); - let resumed = client - .resume_session( - ResumeSessionConfig::new(session_id) - .with_permission_handler(Arc::new(ApproveAllHandler)) - .with_github_token(super::support::DEFAULT_TEST_TOKEN), - ) - .await - .expect("resume session"); - let resumed_workspace = resumed - .rpc() - .workspaces() - .get_workspace() - .await - .expect("get workspace after resume") - .workspace - .expect("workspace after resume"); - assert_eq!( - resumed_workspace.repository.as_deref(), - Some("test-org/test-repo") - ); - assert_eq!( - resumed_workspace.branch.as_deref(), - Some("feature/test-branch") - ); - assert_eq!(resumed_workspace.git_root, workspace.git_root); - assert_eq!(resumed_workspace.cwd, workspace.cwd); - assert_eq!(resumed_workspace.host_type, workspace.host_type); - assert_eq!(resumed_workspace.client_name, workspace.client_name); - - resumed.disconnect().await.expect("disconnect resumed"); - client.stop().await.expect("stop client"); - }) - }) - .await; -} - -#[tokio::test] -async fn should_not_record_git_context_when_host_git_operations_are_disabled() { - super::support::with_e2e_context_no_snapshot(|ctx| { - Box::pin(async move { - ctx.set_default_copilot_user(); - init_git_repository(ctx.work_dir()); - run_git( - ctx.work_dir(), - &[ - "remote", - "add", - "origin", - "https://github.com/test-org/test-repo.git", - ], - ); - run_git( - ctx.work_dir(), - &["commit", "--quiet", "--allow-empty", "-m", "initial"], - ); - let client = ctx.start_client().await; - let session = client - .create_session( - ctx.approve_all_session_config() - .with_enable_host_git_operations(false), - ) - .await - .expect("create session"); - - let workspace = session - .rpc() - .workspaces() - .get_workspace() - .await - .expect("get workspace") - .workspace - .expect("workspace"); - - assert_eq!(workspace.repository, None); - assert_eq!(workspace.branch, None); - assert_eq!(workspace.git_root, None); - assert_eq!(workspace.host_type, None); - - session.disconnect().await.expect("disconnect session"); - client.stop().await.expect("stop client"); - }) - }) - .await; -} - -/// Runs git with the ambient global and system configuration disabled, so a -/// developer's or runner's `commit.gpgsign` / `core.hooksPath` cannot fail the -/// setup of a test that is about recording the workspace's git context. -fn run_git(path: &Path, args: &[&str]) { - let output = Command::new("git") - .args(args) - .current_dir(path) - .env("GIT_CONFIG_NOSYSTEM", "1") - // A path that is never created, so git reads an empty global config. - .env("GIT_CONFIG_GLOBAL", path.join("absent-global-gitconfig")) - .env("GIT_TERMINAL_PROMPT", "0") - .env("GIT_AUTHOR_NAME", "Test") - .env("GIT_AUTHOR_EMAIL", "test@test.com") - .env("GIT_COMMITTER_NAME", "Test") - .env("GIT_COMMITTER_EMAIL", "test@test.com") - .output() - .expect("run git"); - assert!( - output.status.success(), - "git {args:?} should succeed: {}", - String::from_utf8_lossy(&output.stderr) - ); -} - -fn init_git_repository(path: &Path) { - run_git(path, &["init", "--quiet"]); -} -static E2E: super::support::SharedE2eGroup = - super::support::SharedE2eGroup::standard("rpc_workspace_checkpoints", 4); diff --git a/rust/tests/e2e/runtime_host_callbacks.rs b/rust/tests/e2e/runtime_host_callbacks.rs index a47b56874d..92e4ff26b2 100644 --- a/rust/tests/e2e/runtime_host_callbacks.rs +++ b/rust/tests/e2e/runtime_host_callbacks.rs @@ -68,7 +68,7 @@ async fn callback_resume_composes_tools_after_application_session_runtime_restar let (released_tx, mut released) = mpsc::unbounded_channel(); let expected_id = session_id.clone(); let expected_directory = ctx.work_dir().canonicalize().unwrap(); - let options = AhpHostOptions::new() + let options = local_options() .with_create_session(move |_: AhpSessionRequest, _: Client| { creates.fetch_add(1, Ordering::SeqCst); async { Err::, Error>(std::io::Error::other("resume must not create").into()) } @@ -102,7 +102,7 @@ async fn callback_resume_composes_tools_after_application_session_runtime_restar }) .with_on_session_released(move |session| { released_tx.send(session).unwrap(); }); let host = owner.start_ahp_host(options).await.unwrap(); - let ahp = connect_url_as(&host.url, host.token.as_deref(), client_id).await; + let ahp = connect_url_as(host.url.as_deref().expect("local listener URL"), host.token.as_deref(), client_id).await; topology(&host, &owner, &home(ctx).join("ahp/sessions")); resume(&ahp, &uri, None).await; let restored = deadline(resumed.recv()).await.unwrap(); @@ -230,7 +230,7 @@ async fn callback_pending_disposal_releases_without_transport_drain_timeout() { let (released_tx, mut released) = mpsc::unbounded_channel(); let finish = CancellationToken::new(); let gate = finish.clone(); - let options = AhpHostOptions::new() + let options = local_options() .with_create_session(move |request: AhpSessionRequest, client: Client| { let ready_tx = ready_tx.clone(); let gate = gate.clone(); @@ -318,7 +318,7 @@ fn options_with_app( ) { let (created_tx, created) = mpsc::unbounded_channel(); let (released_tx, released) = mpsc::unbounded_channel(); - let options = AhpHostOptions::new() + let options = local_options() .with_create_session(move |request: AhpSessionRequest, client: Client| { let callbacks = callbacks.clone(); let created_tx = created_tx.clone(); @@ -512,7 +512,7 @@ async fn callback_cancellation_releases_late_session_after_host_disposal() { let (released_tx, mut released) = mpsc::unbounded_channel(); let finish = CancellationToken::new(); let gate = finish.clone(); - let options = AhpHostOptions::new() + let options = local_options() .with_create_session(move |request: AhpSessionRequest, client: Client| { let ready_tx = ready_tx.clone(); let gate = gate.clone(); diff --git a/rust/tests/e2e/runtime_host_support.rs b/rust/tests/e2e/runtime_host_support.rs index 00bbecd661..e38ed6ff70 100644 --- a/rust/tests/e2e/runtime_host_support.rs +++ b/rust/tests/e2e/runtime_host_support.rs @@ -151,10 +151,14 @@ pub fn home(ctx: &E2eContext) -> PathBuf { .unwrap() } +pub fn local_options() -> AhpHostOptions { + AhpHostOptions::new().with_local_server(Default::default()) +} + pub fn exit_observer() -> (AhpHostOptions, Arc>>) { let exits = Arc::new(Mutex::new(Vec::new())); let seen = exits.clone(); - let options = AhpHostOptions::default().with_on_exit(move |exit| { + let options = local_options().with_on_exit(move |exit| { seen.lock().unwrap().push(exit); }); (options, exits) @@ -172,10 +176,15 @@ pub async fn exit(exits: &Arc>>) -> AhpHostExit { .await } -pub async fn deadline(future: impl Future) -> T { - tokio::time::timeout(DEADLINE, future) - .await - .expect("AHP operation deadline") +/// Boxes eagerly so callers do not embed large wrapped futures, such as +/// `WebSocketTransport::connect`, whose size depends on consumer TLS features. +pub fn deadline(future: impl Future) -> impl Future { + let future = Box::pin(future); + async move { + tokio::time::timeout(DEADLINE, future) + .await + .expect("AHP operation deadline") + } } pub struct Ahp { @@ -184,7 +193,11 @@ pub struct Ahp { } pub async fn connect(host: &AhpHost) -> Ahp { - connect_url(&host.url, host.token.as_deref()).await + connect_url( + host.url.as_deref().expect("local listener URL"), + host.token.as_deref(), + ) + .await } pub async fn connect_url(url: &str, token: Option<&str>) -> Ahp { @@ -507,7 +520,10 @@ pub fn topology(host: &AhpHost, owner: &Client, catalog: &Path) { Err(error) => panic!("read runtime socket: {error}"), }) .collect(); - let port = reqwest::Url::parse(&host.url).unwrap().port().unwrap(); + let port = reqwest::Url::parse(host.url.as_deref().expect("local listener URL")) + .unwrap() + .port() + .unwrap(); assert!( ["tcp", "tcp6"].iter().any(|table| { std::fs::read_to_string(format!("/proc/{runtime}/net/{table}")) @@ -572,7 +588,7 @@ pub async fn listener_closed(host: &AhpHost, ahp: &Ahp) { deadline(ahp.client.ping()).await.is_err(), "existing AHP client must disconnect" ); - let url = reqwest::Url::parse(&host.url).unwrap(); + let url = reqwest::Url::parse(host.url.as_deref().expect("local listener URL")).unwrap(); let result = deadline(tokio::net::TcpStream::connect(( url.host_str().unwrap().trim_matches(['[', ']']), url.port().unwrap(), 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/subagent_hooks.rs b/rust/tests/e2e/subagent_hooks.rs index 32fdc71d02..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,7 +37,11 @@ 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```"; let (parent_reply, parent_reply_observed) = watch::channel(false); let client = ctx @@ -49,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 @@ -59,14 +68,17 @@ async fn should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls session.subscribe(), "parent waiting reply and subagent result followed by session.idle", |event| { + if !event.agent_id.as_deref().is_none_or(str::is_empty) { + return false; + } if event.parsed_type() == SessionEventType::AssistantMessage { let content = assistant_message_content(event); - if content.contains("Waiting for it to complete...") { + if content == waiting_text { parent_reply .send(true) .expect("sub-agent hook should await the parent reply"); } - if content.contains("Hello from subagent test!") { + if content == final_text { saw_final_response.set(true); } } @@ -83,6 +95,15 @@ async fn should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls completion, ); send_result.expect("send"); + let history = session.get_events().await.expect("get durable history"); + let replies: Vec<_> = history.iter() + .filter(|event| event.agent_id.as_deref().is_none_or(str::is_empty) + && event.parsed_type() == SessionEventType::AssistantMessage) + .map(assistant_message_content) + .filter(|content| *content == waiting_text || *content == final_text) + .collect(); + assert_eq!(replies, [waiting_text, final_text], + "durable history must contain the waiting reply before the final reply"); let log = hook_log.lock().clone(); @@ -119,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"); @@ -142,6 +232,7 @@ struct RequestEntry { agent_id: Option, parent_agent_id: Option, interaction_type: Option, + body: String, } #[derive(Default)] @@ -172,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 } @@ -223,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 { @@ -248,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 0d45e2c8b4..d352aa3131 100644 --- a/rust/tests/e2e/support.rs +++ b/rust/tests/e2e/support.rs @@ -3,6 +3,8 @@ use std::future::Future; use std::io::{BufRead, BufReader, Read, Write}; use std::net::{TcpStream, ToSocketAddrs}; use std::ops::Deref; +#[cfg(windows)] +use std::os::windows::process::CommandExt; use std::panic::AssertUnwindSafe; use std::path::{Path, PathBuf}; use std::pin::Pin; @@ -337,28 +339,6 @@ pub async fn with_dedicated_group_e2e_context( with_dedicated_e2e_context(category, snapshot_name, test).await; } -pub async fn skip_shared_e2e_inprocess(group: &'static SharedE2eGroup, reason: &str) -> bool { - if !skip_inprocess(reason) { - return false; - } - - let mut state = group.state.lock().await; - let _permit = E2E_CONCURRENCY - .acquire() - .await - .expect("E2E concurrency semaphore should stay open"); - let completed = group.completed_invocations.fetch_add(1, Ordering::Relaxed) + 1; - if completed == group.expected_invocations - && let Some(state) = state.take() - { - state - .shutdown_bounded(false) - .await - .unwrap_or_else(|error| panic!("tear down shared E2E group after skip: {error}")); - } - true -} - /// Run a dedicated one-client E2E test. /// /// New tests should call [`with_dedicated_e2e_context`] to make the lifecycle @@ -1321,14 +1301,20 @@ struct CapiProxy { impl CapiProxy { fn start(repo_root: &Path) -> std::io::Result { - let mut child = Command::new(npx_program()) - .args(["tsx", "server.ts"]) + let mut command = Command::new(node_program()); + command + .args(["--import", "tsx", "server.ts"]) .current_dir(repo_root.join("test").join("harness")) .env("GITHUB_ACTIONS", "true") .stdin(Stdio::null()) .stdout(Stdio::piped()) - .stderr(Stdio::inherit()) - .spawn()?; + .stderr(Stdio::inherit()); + #[cfg(windows)] + { + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + command.creation_flags(CREATE_NO_WINDOW); + } + let mut child = command.spawn()?; let stdout = child.stdout.take().expect("proxy stdout"); let (line_tx, line_rx) = std::sync::mpsc::channel(); @@ -1585,10 +1571,6 @@ fn node_program() -> &'static str { if cfg!(windows) { "node.exe" } else { "node" } } -fn npx_program() -> &'static str { - if cfg!(windows) { "npx.cmd" } else { "npx" } -} - #[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/runtime_host_e2e.rs b/rust/tests/runtime_host_e2e.rs index 43496b810b..fab6e8724d 100644 --- a/rust/tests/runtime_host_e2e.rs +++ b/rust/tests/runtime_host_e2e.rs @@ -14,7 +14,7 @@ mod callbacks; use std::time::Duration; use ahp_ws::WebSocketTransport; -use github_copilot_sdk::rpc::{HostDisposeRequest, HostExitReason}; +use github_copilot_sdk::rpc::{HostDisposeRequest, HostExitReason, HostLocalServerOptions}; use github_copilot_sdk::{AhpHostOptions, Client, ResumeSessionConfig, SessionId, Transport}; use host_support::*; use serde_json::{Value, json}; @@ -34,10 +34,7 @@ async fn streams_real_ahp_turn_beside_sdk_session_on_same_runtime() { ) .await .unwrap(); - let host = owner - .start_ahp_host(AhpHostOptions::default()) - .await - .unwrap(); + let host = owner.start_ahp_host(local_options()).await.unwrap(); let ahp = connect(&host).await; let (uri, chat, subscription) = create(&ahp, ctx).await; topology(&host, &owner, &home(ctx).join("ahp/sessions")); @@ -114,10 +111,7 @@ async fn durable_catalog_resumes_after_repeated_listener_disposal() { .create_session(ctx.approve_all_session_config()) .await .unwrap(); - let host = owner - .start_ahp_host(AhpHostOptions::default()) - .await - .unwrap(); + let host = owner.start_ahp_host(local_options()).await.unwrap(); let ahp = connect(&host).await; let (uri, chat, subscription) = create(&ahp, ctx).await; turn(&ahp, &chat, subscription).await; @@ -132,10 +126,7 @@ async fn durable_catalog_resumes_after_repeated_listener_disposal() { assert_eq!(exit(&exits).await.reason, HostExitReason::Disposed); stopped(&replacement, &ahp, &owner).await; ahp.client.shutdown().await; - let recovered = owner - .start_ahp_host(AhpHostOptions::default()) - .await - .unwrap(); + let recovered = owner.start_ahp_host(local_options()).await.unwrap(); let ahp = connect(&recovered).await; resume(&ahp, &uri, Some(sdk.id().as_str())).await; sdk.get_events().await.unwrap(); @@ -162,10 +153,7 @@ async fn explicit_base_directory_survives_runtime_restart_and_excludes_other_wri let second = Client::start(options_with_base_directory(ctx, &base)) .await .unwrap(); - let host = first - .start_ahp_host(AhpHostOptions::default()) - .await - .unwrap(); + let host = first.start_ahp_host(local_options()).await.unwrap(); let ahp = connect(&host).await; topology(&host, &first, &base.join("ahp/sessions")); let (uri, chat, subscription) = create(&ahp, ctx).await; @@ -174,11 +162,7 @@ async fn explicit_base_directory_survives_runtime_restart_and_excludes_other_wri .create_session(ctx.approve_all_session_config()) .await .unwrap(); - let error = second - .start_ahp_host(AhpHostOptions::default()) - .await - .err() - .unwrap(); + let error = second.start_ahp_host(local_options()).await.err().unwrap(); assert!( error.to_string().contains("catalog") && error.to_string().contains("already in use"), @@ -196,10 +180,7 @@ async fn explicit_base_directory_survives_runtime_restart_and_excludes_other_wri let restarted = Client::start(options_with_base_directory(ctx, &base)) .await .unwrap(); - let host = restarted - .start_ahp_host(AhpHostOptions::default()) - .await - .unwrap(); + let host = restarted.start_ahp_host(local_options()).await.unwrap(); let ahp = connect(&host).await; resume(&ahp, &uri, None).await; host.dispose().await.unwrap(); @@ -251,11 +232,7 @@ async fn same_catalog_and_other_owner_rejected_disconnect_cleans_up_without_stop let (uri, chat, subscription) = create(&ahp, ctx).await; turn(&ahp, &chat, subscription).await; topology(&host, &owner, &home(ctx).join("ahp/sessions")); - let error = owner - .start_ahp_host(AhpHostOptions::default()) - .await - .err() - .unwrap(); + let error = owner.start_ahp_host(local_options()).await.err().unwrap(); assert!( error.to_string().contains("catalog") && error.to_string().contains("already in use"), @@ -290,10 +267,7 @@ async fn same_catalog_and_other_owner_rejected_disconnect_cleans_up_without_stop stopped_after_owner_disconnect(&host, &ahp, &owner).await; ahp.client.shutdown().await; sdk.get_events().await.unwrap(); - let replacement = owner - .start_ahp_host(AhpHostOptions::default()) - .await - .unwrap(); + let replacement = owner.start_ahp_host(local_options()).await.unwrap(); let ahp = connect(&replacement).await; resume(&ahp, &uri, Some(sdk.id().as_str())).await; replacement.dispose().await.unwrap(); @@ -326,7 +300,12 @@ async fn listener_startup_failure_recovers_and_preserves_owner_session() { let port = blocker.local_addr().unwrap().port(); assert!( owner - .start_ahp_host(AhpHostOptions::default().with_port(port.into())) + .start_ahp_host(AhpHostOptions::default().with_local_server( + HostLocalServerOptions { + port: Some(port.into()), + ..Default::default() + } + )) .await .is_err() ); @@ -334,7 +313,10 @@ async fn listener_startup_failure_recovers_and_preserves_owner_session() { drop(blocker); // Listener recovery is not process isolation: it runs inside the runtime. let host = owner - .start_ahp_host(options.with_port(port.into())) + .start_ahp_host(options.with_local_server(HostLocalServerOptions { + port: Some(port.into()), + ..Default::default() + })) .await .unwrap(); let ahp = connect(&host).await; @@ -407,31 +389,33 @@ async fn listener_defaults_explicit_ports_tokens_and_invalid_combinations() { run(|ctx| { Box::pin(async move { let owner = start(ctx).await; - let host = owner - .start_ahp_host(AhpHostOptions::default()) - .await - .unwrap(); - let url = reqwest::Url::parse(&host.url).unwrap(); + let host = owner.start_ahp_host(local_options()).await.unwrap(); + let url = + reqwest::Url::parse(host.url.as_deref().expect("local listener URL")).unwrap(); assert_eq!(url.host_str(), Some("127.0.0.1")); assert!(url.port().unwrap() > 0); assert!(!host.token.as_ref().unwrap().is_empty()); assert!( - deadline(WebSocketTransport::connect(&host.url)) - .await - .is_err() + deadline(WebSocketTransport::connect( + host.url.as_deref().expect("local listener URL") + )) + .await + .is_err() ); let ahp = connect(&host).await; host.dispose().await.unwrap(); stopped(&host, &ahp, &owner).await; ahp.client.shutdown().await; for (hostname, port) in [("0.0.0.0", unused_port()), ("localhost", 0), ("::1", 0)] { - let options = AhpHostOptions::default() - .with_hostname(hostname) - .with_port(port.into()) - .with_token("rust-supplied-token") - .with_require_connection_token(true); + let options = AhpHostOptions::default().with_local_server(HostLocalServerOptions { + hostname: Some(hostname.into()), + port: Some(port.into()), + token: Some("rust-supplied-token".into()), + require_connection_token: Some(true), + }); let host = owner.start_ahp_host(options).await.unwrap(); - let mut url = reqwest::Url::parse(&host.url).unwrap(); + let mut url = + reqwest::Url::parse(host.url.as_deref().expect("local listener URL")).unwrap(); match hostname { "0.0.0.0" => assert_eq!(url.host_str(), Some("0.0.0.0")), "::1" => assert_eq!(url.host_str(), Some("[::1]")), @@ -474,16 +458,13 @@ async fn listener_defaults_explicit_ports_tokens_and_invalid_combinations() { json!({"port": 1.5}), json!({"hostname": ""}), ] { - let mut request = invalid; + let mut request = json!({"localServer": invalid}); request["hostId"] = json!(uuid::Uuid::new_v4().to_string()); // The raw RPC entry point also reaches runtime validation for // fractional ports, which the generated Rust integer rejects. assert!(owner.call("host.start", Some(request)).await.is_err()); } - let host = owner - .start_ahp_host(AhpHostOptions::default()) - .await - .unwrap(); + let host = owner.start_ahp_host(local_options()).await.unwrap(); host.dispose().await.unwrap(); owner.stop().await.unwrap(); }) @@ -499,7 +480,12 @@ async fn disabled_connection_token_still_requires_ahp_resource_authentication() Box::pin(async move { let owner = start(ctx).await; let host = owner - .start_ahp_host(AhpHostOptions::default().with_require_connection_token(false)) + .start_ahp_host(AhpHostOptions::default().with_local_server( + HostLocalServerOptions { + require_connection_token: Some(false), + ..Default::default() + }, + )) .await .unwrap(); assert_eq!(host.token, None); 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..23a254d9e9 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::{ @@ -5373,7 +5374,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 +5387,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 +5761,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 +6902,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 +7183,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 +7195,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 +7287,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 +7313,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 +7409,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 +7831,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/samples/runtime-host/smoke.ts b/samples/runtime-host/smoke.ts index 47178828b5..2f785a56c1 100644 --- a/samples/runtime-host/smoke.ts +++ b/samples/runtime-host/smoke.ts @@ -37,6 +37,7 @@ try { model: "claude-sonnet-5", }); await using host = await owner.startAhpHost({ + localServer: {}, onExit: (exit) => console.log(`AHP host stopped: ${exit.reason}`), }); const ahp = await connectAhp(host); @@ -46,6 +47,7 @@ try { assert(runtime.pid); await assertRuntimeListener(host, runtime.pid, artifacts); console.log(`SDK app ${process.pid} -> runtime ${runtime.pid} (in-process AHP listener)`); + if (!host.url) throw new Error("Local hosting must return a listener URL"); const listener = new URL(host.url); listener.search = ""; console.log(`AHP listener: ${listener} (connection token intentionally not printed)`); 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 a92f6a5679..ea0312c4a6 100755 --- a/scripts/ci/run-dotnet-tests.sh +++ b/scripts/ci/run-dotnet-tests.sh @@ -1,9 +1,29 @@ #!/usr/bin/env bash # Copyright (c) Microsoft Corporation. All rights reserved. -# Runs the requested .NET SDK test shard with optional backend filtering. +# Runs .NET SDK tests with optional backend filtering. set -euo pipefail +usage() { + cat <<'EOF' +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 +} + +if (($# == 1)) && [[ "$1" == "--help" ]]; then + usage + exit 0 +fi +if (($# != 0)); then + usage >&2 + exit 2 +fi + script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" cd "$script_dir/../../dotnet" @@ -15,106 +35,12 @@ 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:-}" -shard="${DOTNET_TEST_SHARD:-full}" runtime="${DOTNET_TEST_RUNTIME:-}" -if [[ "$shard" != "full" ]]; then - case "$shard" in - 1) - initials="A C D H I J K L M N Q S U W Y" - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.ConnectionToken" - ;; - 2) - initials="B E F G O P R T V X Z" - shard_filter="" - ;; - 2a) - initials="B E F G" - shard_filter="" - ;; - 2b-pending) - initials="" - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.PendingWorkResumeE2ETests" - ;; - 2b-permission) - initials="" - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.PermissionE2ETests" - ;; - 2b-auth) - initials="" - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.PerSessionAuthE2ETests" - ;; - 2b-hooks) - initials="" - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.PreMcpToolCallHookE2ETests" - ;; - 2b-unit-p) - initials="" - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.Unit.P" - ;; - 2b-provider) - initials="O" - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.ProviderEndpointE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RewindE2ETests|FullyQualifiedName~GitHub.Copilot.Test.Unit.R" - ;; - 2b-rpc-additional) - initials="" - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcAdditionalEdgeCasesE2ETests" - ;; - 2b-rpc-agent) - initials="" - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcAgentE2ETests" - ;; - 2b-rpc-event-log) - initials="" - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcEventLogE2ETests" - ;; - 2b-rpc-event-effects) - initials="" - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcEventSideEffectsE2ETests" - ;; - 2b-rpc-mcp-skills) - initials="" - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpAndSkillsE2ETests" - ;; - 2b-rpc-mcp-config) - initials="" - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpConfigE2ETests" - ;; - 2b-rpc-mcp-lifecycle) - initials="" - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpLifecycleE2ETests" - ;; - 2b-rpc-q-z) - initials="" - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcQueueE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcRemoteE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcScheduleE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcServerE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcServerMiscE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcServerPluginsE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcServerRemoteControlE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcSessionStateE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcSessionStateExtrasE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcShellAndFleetE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcShellEdgeCaseE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcShellUserRequestedE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcTasksAndHandlersE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcUiEphemeralQueryE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcWorkspaceCheckpointsE2ETests" - ;; - extensions) - initials="" - shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcExtensionsLoadedE2ETests" - ;; - 2c) - initials="T V X Z" - shard_filter="" - ;; - *) - echo "Unknown .NET test shard: $shard" >&2 - exit 2 - ;; - esac - - for namespace in E2E Unit; do - for initial in $initials; do - clause="FullyQualifiedName~GitHub.Copilot.Test.${namespace}.${initial}" - shard_filter="${shard_filter:+${shard_filter}|}${clause}" - done - done - filter="${filter:+(${filter})&}(${shard_filter})" -fi - if [[ -n "$filter" ]]; then args+=(--filter "$filter") fi diff --git a/scripts/ci/run-dotnet-tests.test.mjs b/scripts/ci/run-dotnet-tests.test.mjs index 2d7ee32dfd..582fcd6b2c 100644 --- a/scripts/ci/run-dotnet-tests.test.mjs +++ b/scripts/ci/run-dotnet-tests.test.mjs @@ -2,8 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -/** Verifies .NET test shard selection and filter composition. */ - import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import fs from "node:fs"; @@ -14,8 +12,15 @@ import { fileURLToPath } from "node:url"; const script = fileURLToPath(new URL("./run-dotnet-tests.sh", import.meta.url)); -await test("constructs a focused filter for named .NET shards", (t) => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), "sdk-dotnet-shard-")); +function fromBashPath(value) { + if (process.platform === "win32" && /^\/[a-z]\//i.test(value)) { + return `${value[1].toUpperCase()}:${value.slice(2)}`; + } + return value; +} + +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"); const workingDirectoryPath = path.join(root, "working-directory"); @@ -26,70 +31,53 @@ await test("constructs a focused filter for named .NET shards", (t) => { ); fs.chmodSync(fakeDotnet, 0o755); - const result = spawnSync("bash", [script], { - encoding: "utf8", - env: { - ...process.env, - ARGUMENTS_PATH: argumentsPath, - DOTNET_TEST_FILTER: "E2EBackend!=CapiOnly", - DOTNET_TEST_SHARD: "2b-rpc-agent", - PATH: `${root}${path.delimiter}${process.env.PATH ?? ""}`, - WORKING_DIRECTORY_PATH: workingDirectoryPath, - }, - }); + for (const [filter, runtime, resultsDirectory] of [ + ["", "", ""], + ["E2EBackend!=CapiOnly", "win-x64", "TestResults/inprocess"], + ]) { + const result = spawnSync("bash", [script], { + encoding: "utf8", + env: { + ...process.env, + 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, + }, + }); - assert.equal(result.status, 0, result.stderr); - assert.equal( - fs.realpathSync(fs.readFileSync(workingDirectoryPath, "utf8").trim()), - fs.realpathSync(fileURLToPath(new URL("../../dotnet", import.meta.url))), - ); - const arguments_ = fs.readFileSync(argumentsPath, "utf8").trim().split("\n"); - assert.deepEqual(arguments_.slice(0, 3), [ - "test", - "test/GitHub.Copilot.SDK.Test.csproj", - "--no-build", - ]); - const filterIndex = arguments_.indexOf("--filter"); - assert.notEqual(filterIndex, -1); - assert.equal( - arguments_[filterIndex + 1], - "(E2EBackend!=CapiOnly)&(FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcAgentE2ETests)", - ); + assert.equal(result.status, 0, result.stderr); + assert.equal( + fs.realpathSync(fromBashPath(fs.readFileSync(workingDirectoryPath, "utf8").trim())), + fs.realpathSync(fileURLToPath(new URL("../../dotnet", import.meta.url))), + ); + 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); + } + assert.equal(arguments_.includes("--runtime"), runtime !== ""); + if (runtime) { + assert.equal(arguments_[arguments_.indexOf("--runtime") + 1], runtime); + } + } }); -await test("rejects unknown .NET shards before invoking dotnet", () => { - const result = spawnSync("bash", [script], { - encoding: "utf8", - env: { ...process.env, DOTNET_TEST_SHARD: "unknown" }, - }); +await test("documents its environment and rejects positional arguments", () => { + const help = spawnSync("bash", [script, "--help"], { encoding: "utf8" }); + 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/); - assert.equal(result.status, 2); - assert.match(result.stderr, /Unknown \.NET test shard: unknown/); -}); - -await test("constructs the extensions shard without initial buckets", (t) => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), "sdk-dotnet-extensions-")); - t.after(() => fs.rmSync(root, { recursive: true, force: true })); - const argumentsPath = path.join(root, "arguments"); - const fakeDotnet = path.join(root, "dotnet"); - fs.writeFileSync(fakeDotnet, `#!/usr/bin/env bash\nprintf '%s\\n' "$@" > "$ARGUMENTS_PATH"\n`); - fs.chmodSync(fakeDotnet, 0o755); - - const result = spawnSync("bash", [script], { - encoding: "utf8", - env: { - ...process.env, - ARGUMENTS_PATH: argumentsPath, - DOTNET_TEST_SHARD: "extensions", - PATH: `${root}${path.delimiter}${process.env.PATH ?? ""}`, - }, - }); - - assert.equal(result.status, 0, result.stderr); - const arguments_ = fs.readFileSync(argumentsPath, "utf8").trim().split("\n"); - const filterIndex = arguments_.indexOf("--filter"); - assert.equal( - arguments_[filterIndex + 1], - "(FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcExtensionsLoadedE2ETests)", - ); + const invalid = spawnSync("bash", [script, "unexpected"], { encoding: "utf8" }); + assert.equal(invalid.status, 2); + assert.match(invalid.stderr, /Usage: run-dotnet-tests\.sh/); }); diff --git a/scripts/ci/run-tasks.test.mjs b/scripts/ci/run-tasks.test.mjs index e4c5bfd7c9..99c456e3c5 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,98 @@ 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))); + }); +} + +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..1c59d64f18 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))]`); @@ -3015,7 +3108,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 fbbb3895d3..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}`); @@ -4278,6 +4360,12 @@ function emitMethod(lines: string[], receiver: string, name: string, method: Rpc } lines.push(`\traw, err := ${clientRef}.Request(ctx, "${method.rpcMethod}", req)`); } else { + if (method.rpcMethod === "managedSettings.resolve") { + // A typed nil inside Request's any argument marshals as null, not omitted params. + lines.push(`\tif ${paramsRef} == nil {`); + lines.push(`\t\t${paramsRef} = &${paramsType}{}`); + lines.push(`\t}`); + } const arg = hasParams ? paramsRef : "nil"; lines.push(`\traw, err := ${clientRef}.Request(ctx, "${method.rpcMethod}", ${arg})`); } @@ -4292,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(``); @@ -4582,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..787fe2ca6e 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, @@ -290,6 +290,47 @@ 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; + for (const [propertyName, property] of Object.entries(definition.properties ?? {})) { + if ( + typeof property !== "object" || + property["x-copilot-sdk-append-last"] !== true || + seen.has(`${className}.${propertyName}`) + ) { + continue; + } + result.push([className, propertyName]); + } + } + + return result; +} + function preservePythonRpcStringDateFields(definitions: Record): void { const quotaSnapshot = definitions.AccountQuotaSnapshot; const resetDate = quotaSnapshot?.properties?.resetDate as JSONSchema7 | undefined; @@ -1343,7 +1384,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 +1396,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 +1563,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 +1694,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 +1765,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 +1778,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 +1790,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; @@ -2171,6 +2313,17 @@ 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)`); + } ctx.enumsByName.set(enumName, enumName); ctx.enums.push(lines.join("\n")); return enumName; @@ -3156,6 +3309,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"); @@ -3296,6 +3467,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) @@ -3349,12 +3524,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 +3615,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 +4445,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/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..63ca1b77e2 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: [ @@ -151,6 +152,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 +310,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 +347,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/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..dd5627df3a 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -123,6 +123,7 @@ const defaultModel = "claude-sonnet-5"; */ export class ReplayingCapiProxy extends CapturingHttpProxy { private state: ReplayingCapiProxyState | null = null; + private memoryApiStub: MemoryApiStub | undefined; private startPromise: Promise | null = null; private defaultToolResultNormalizers: ToolResultNormalizer[] = [ { toolName: "*", normalizer: normalizeLargeOutputFilepaths }, @@ -201,6 +202,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 +294,16 @@ 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; + } + // Handle /config endpoint for updating proxy configuration if ( options.requestOptions.path === "/config" && @@ -324,20 +336,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 +446,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 +530,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 +547,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 +1061,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 +1100,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 +1187,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 +1200,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 +1229,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 +1258,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 +1408,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 +1488,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 +1503,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 +1564,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 +1579,7 @@ function transformOpenAIRequestMessage( } function normalizeUserMessage(content: string): string { - return normalizeSkillContextFrontmatter(content) + return stripModeChangedNotice(normalizeSkillContext(content)) .replace( taskCompletionNotificationPattern, taskCompletionNotificationReplacement, @@ -1408,16 +1601,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 +1711,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 +1809,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 +1905,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 +2012,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 +2182,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 +2232,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 +2250,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 +2268,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 +2283,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 +2321,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 +2376,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 +2457,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 +2495,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